mirror of
https://github.com/langgenius/dify.git
synced 2026-05-23 10:29:07 +08:00
Compare commits
1096 Commits
deploy/age
...
laipz8200/
| Author | SHA1 | Date | |
|---|---|---|---|
| 43ed1dc14c | |||
| a9a375000a | |||
| 4111f33ad3 | |||
| 0a5952ae3f | |||
| 2a0c098857 | |||
| 790ca72627 | |||
| 4d8b6c7dc0 | |||
| 473c945839 | |||
| a698c60b29 | |||
| 24bab5fb2a | |||
| 93b7a81071 | |||
| 157e6244dd | |||
| 964aaad7ed | |||
| 92181dbe09 | |||
| 30deef45d9 | |||
| ee28074390 | |||
| 1fb491337b | |||
| 82b0a03f5a | |||
| 6185016910 | |||
| b4f5f4869f | |||
| 7ecbed3b04 | |||
| 5b58defd62 | |||
| 73196de5e1 | |||
| ea5e487d3c | |||
| f19702f76c | |||
| 092c8bca81 | |||
| c50d504c44 | |||
| 1b4356b66a | |||
| 7f633622aa | |||
| 66f5ab4cfc | |||
| 0cf9597f52 | |||
| 60cd346fa6 | |||
| 56d4d54c16 | |||
| 9f9cb4d17e | |||
| 7d0d9019d8 | |||
| d646bcf257 | |||
| e3b45a48eb | |||
| 848c15a265 | |||
| be8627233d | |||
| 1fe8b7fb1d | |||
| 5a585c8618 | |||
| cc9b90a5ae | |||
| b64d4b53ca | |||
| 5cdf4e405b | |||
| 7cb14cb4cc | |||
| de38bba99b | |||
| f04d809426 | |||
| 7ed3c7c500 | |||
| 77f1aeb1ac | |||
| 7bc5c89e3c | |||
| 718ab8433e | |||
| 8f197c5a0a | |||
| 0295862d0d | |||
| 2b2a5824c1 | |||
| 468cc19e68 | |||
| 77333e57a7 | |||
| f52491e2c1 | |||
| 05408af8a1 | |||
| d3ae074456 | |||
| 0b48a7e991 | |||
| 809f513ccb | |||
| d9e90d0fa0 | |||
| d1417bbe4b | |||
| 2565637e36 | |||
| cae9923e5a | |||
| a328bbbced | |||
| 5276eb689b | |||
| 4b2badb6f2 | |||
| 34a89416f7 | |||
| a13ab76002 | |||
| b04b4449db | |||
| 674cdc3521 | |||
| 2031d31ee8 | |||
| 04d62867af | |||
| 7f392b6950 | |||
| b0a3399774 | |||
| 2d5186fb28 | |||
| 06f076e0ff | |||
| 5b79f7e99d | |||
| 1cee1a25b6 | |||
| c0f237bf35 | |||
| 75d7fc0526 | |||
| c057b5c5ff | |||
| 5468c4ec96 | |||
| f4c02e4c6b | |||
| 9dc95eeb20 | |||
| 76bba64b79 | |||
| 59e96fbb2a | |||
| 06ea0f7ac2 | |||
| 730a0bef9e | |||
| 2eb37caf2e | |||
| 7e8147295b | |||
| c07686928a | |||
| d1238180ed | |||
| 969760364d | |||
| ceabfeb3a7 | |||
| c407f40e0d | |||
| 28818f2e2a | |||
| e2c52c9b0f | |||
| 1925d58369 | |||
| b79fc5d6b4 | |||
| 6649e4025e | |||
| b96f372f45 | |||
| 127fbf2c9a | |||
| 3c70d28064 | |||
| cd4d6f8a22 | |||
| 9d0906c684 | |||
| 41b6f894c0 | |||
| e7e6fe8813 | |||
| c0bdd6792f | |||
| 27b084c4d4 | |||
| 3f7a68fc77 | |||
| a252fbddfa | |||
| ff02636a4b | |||
| 63946d829e | |||
| cdcfd2ef2c | |||
| b04a3851cc | |||
| b41338cd08 | |||
| 28153df4d3 | |||
| 3bc3386535 | |||
| 7654f14241 | |||
| 194b54bae4 | |||
| 0e16d36edb | |||
| 432a6412a3 | |||
| 55d05fe52d | |||
| 0d500e6965 | |||
| 5798610f27 | |||
| a35b28dbef | |||
| 1a4288c811 | |||
| 9dc32f2318 | |||
| 7210f856c9 | |||
| ebcc1200a3 | |||
| e660d7af38 | |||
| d9ccfcbc6e | |||
| a9bcec013f | |||
| aeb7687e2c | |||
| 9355d36718 | |||
| a03ee828a3 | |||
| 7066372892 | |||
| 55f95dbc36 | |||
| 8b40de3c4e | |||
| af4b9bfa8f | |||
| b9e3130388 | |||
| 12d33652b6 | |||
| fe8cf2aff4 | |||
| d1d190374d | |||
| e1be4e6aa8 | |||
| 301a470e7a | |||
| 91251ad5a5 | |||
| 3f6644a615 | |||
| 5edc682c4a | |||
| 13c00ecfc4 | |||
| 9d545144ce | |||
| 2afa39cdcb | |||
| bb1c883be4 | |||
| 03861bcee3 | |||
| c34fc429ae | |||
| d110112863 | |||
| 934a20e745 | |||
| 7e56a244a8 | |||
| 6facd9360c | |||
| a18d7f51eb | |||
| 680ef077ae | |||
| c26be9d3f4 | |||
| 51a8f79d67 | |||
| bb73776339 | |||
| 9424bf60b0 | |||
| cbedcd2882 | |||
| 1a93af5cd0 | |||
| cd90d7ffc1 | |||
| 4bb987eca3 | |||
| 4fd4615c56 | |||
| c7d30bf09a | |||
| 59dab7deac | |||
| a60cb3b800 | |||
| 6164408da1 | |||
| 7fc40e6c9e | |||
| d625ac0bf1 | |||
| 1082f488a1 | |||
| f1c4c1a5ff | |||
| dd1cdbbd41 | |||
| 74a04afe27 | |||
| b108ea42f6 | |||
| 1aa6188b7d | |||
| bd0d10ac5c | |||
| 2162ea6a68 | |||
| 153064bbd4 | |||
| a643b05368 | |||
| 279b66bc7f | |||
| e134c1e0d5 | |||
| 9127209dd5 | |||
| a2ee151e48 | |||
| 9e3e616391 | |||
| 837b5cad86 | |||
| 1a011dc14a | |||
| bf117dd0c8 | |||
| 1e6dc62470 | |||
| 0b70eec695 | |||
| e8dc706414 | |||
| 9a2bea9287 | |||
| b95e6f6a7a | |||
| b99ba74aa4 | |||
| 7b5c371b9d | |||
| c67ce6f66d | |||
| e48d7bb097 | |||
| 24ea21db25 | |||
| 8581a68174 | |||
| f720a3bed2 | |||
| 4a56763d2f | |||
| 861f73267c | |||
| 1efd365b62 | |||
| 65c36a51ef | |||
| 19476109da | |||
| f3eb3ab4dd | |||
| 2c9e30426d | |||
| 2bb1f0906b | |||
| d5ad6aedc0 | |||
| 5ebeb34feb | |||
| c5ac191a79 | |||
| 140ad6ba4e | |||
| e03eb3a76c | |||
| 38a419d073 | |||
| c74cbb68da | |||
| 271019006e | |||
| 19bf36a716 | |||
| 48d27e250b | |||
| d06b5529b3 | |||
| 8132c444dc | |||
| cb0356e9d7 | |||
| 4d80892d7b | |||
| af754f497a | |||
| 8f93bb36ba | |||
| 82f24b336d | |||
| 927a17804b | |||
| 29f34848cd | |||
| 1b0d4637b3 | |||
| 936a09c704 | |||
| 5cc62fd1c9 | |||
| 7bc19d8251 | |||
| e845475408 | |||
| 9a8aa6a0c3 | |||
| 76a7f5f4b9 | |||
| 2ff50514c8 | |||
| 7901ac9a97 | |||
| ecd830083a | |||
| 203b3a9499 | |||
| 9331024d91 | |||
| c6a5de3c18 | |||
| cd3327013a | |||
| cd66559ebf | |||
| 8b77ec7f31 | |||
| bb3de5dd32 | |||
| 1e2d309122 | |||
| a24ec60e51 | |||
| 8fd616d27f | |||
| e5bdc40dce | |||
| 376c43e5ac | |||
| 3ebb449d25 | |||
| 5297ac76ec | |||
| bbed1d4a7c | |||
| c804dbed8c | |||
| 00bf3f83f2 | |||
| 7e6745e105 | |||
| d648ce6888 | |||
| f3c3534e33 | |||
| 8967ff34b3 | |||
| 57539792c1 | |||
| 03e227f8f1 | |||
| 506e1a8bc7 | |||
| f8873ec07b | |||
| b2dacf0718 | |||
| 70eb98d6c5 | |||
| b83f296634 | |||
| 5c68f12bb8 | |||
| 4df7c00859 | |||
| 995c43f3dd | |||
| c0431ec843 | |||
| a0af10abc8 | |||
| 8e2b8168be | |||
| 1f29565673 | |||
| 90fe54ca9e | |||
| b43ebf539d | |||
| 853b859032 | |||
| 8f3e42e9c2 | |||
| 1359c03216 | |||
| 4b7dc17546 | |||
| 81090effe2 | |||
| d92c336394 | |||
| cd9daef564 | |||
| 2876839d7e | |||
| 7ba408eebe | |||
| 3708e3eef1 | |||
| ff5c2c57a1 | |||
| 955c25589d | |||
| 54bde0bdf6 | |||
| 87add9a4f3 | |||
| 574d5865f4 | |||
| 458fab1c48 | |||
| 88196c186e | |||
| dcf21a6a84 | |||
| 91f92c7083 | |||
| 0ca339103f | |||
| 5cf741895f | |||
| 11c52e90f6 | |||
| f01e099729 | |||
| 195ff4711d | |||
| fe2f7a8920 | |||
| 3b1458c08f | |||
| 9f47317032 | |||
| e751ec323e | |||
| f1d72eb5d2 | |||
| 44242d03b4 | |||
| ed7ea68f7d | |||
| afbc30c9ed | |||
| 0e55dcb297 | |||
| 25973c7d77 | |||
| 73ecdd5494 | |||
| 6fafeec415 | |||
| d23cefe005 | |||
| 16d408d908 | |||
| 0536549f73 | |||
| d0956039e7 | |||
| 38eb04dc98 | |||
| d2e1da269c | |||
| 1d3498f659 | |||
| b8dea56198 | |||
| e2becd6746 | |||
| 28a26f2d59 | |||
| 8c7393ef46 | |||
| 5a7a955210 | |||
| 3e4849d765 | |||
| 0c280ef708 | |||
| 282561a861 | |||
| cbb4cc5d76 | |||
| 2d6babeeb4 | |||
| 1065a4840a | |||
| b6aa5a7d69 | |||
| 949f930698 | |||
| 65a08ed7ab | |||
| cc4d6db7c8 | |||
| 6b5d6dacb2 | |||
| 89bf75eba9 | |||
| 3a28868a6c | |||
| 4036515abe | |||
| 6c089cab66 | |||
| 818a71d637 | |||
| 3db107edc9 | |||
| 2677d90860 | |||
| 859756c4f6 | |||
| 295fb6e74a | |||
| 2326fb7a83 | |||
| 2d6eaf69f9 | |||
| 3e826c0000 | |||
| b1b977e284 | |||
| 23648141c9 | |||
| d6dee43c09 | |||
| 7efc887e32 | |||
| 8b346e69d9 | |||
| ef7ff3356d | |||
| 7b5c0b5045 | |||
| f00512dd5d | |||
| e6ef774fd5 | |||
| ce50c6cf1c | |||
| 7002512106 | |||
| c3aebb8403 | |||
| 0baefa6163 | |||
| 7bcedcbaab | |||
| 791fc5819d | |||
| 2d09c4788d | |||
| 9bd5c2f8ec | |||
| 5e336c47fd | |||
| be4c828214 | |||
| ec450eb7f9 | |||
| 48e13f65dc | |||
| 38fc2a6574 | |||
| ed8d3f3e8d | |||
| 0c8dec3315 | |||
| 38e831c1b3 | |||
| 1c5d62d98a | |||
| 6b4736bf78 | |||
| c9503fd818 | |||
| 91a1df96cb | |||
| 5b2c5da945 | |||
| b59ecea346 | |||
| 61c0948136 | |||
| f746c7bdf2 | |||
| 2a3deee385 | |||
| 4b6803ba06 | |||
| 4c908c8f39 | |||
| afec528f51 | |||
| 491061b8f4 | |||
| 8b1533438f | |||
| ba924fc97b | |||
| 712e522220 | |||
| 33eebe8cfc | |||
| 2e1b11bdb2 | |||
| d65a6b4810 | |||
| 44a91e344c | |||
| 0fec9af6a6 | |||
| 5e5113e08e | |||
| 73f9a9e7d6 | |||
| 48d23cd744 | |||
| 0b60bf6ef0 | |||
| 3b24d8d2d1 | |||
| 051ba99cd2 | |||
| dc83e8aa09 | |||
| 77f8f2babb | |||
| 77d6c108e7 | |||
| c2a5962023 | |||
| d583b1b835 | |||
| da00de6688 | |||
| a633387e9b | |||
| df389eba1c | |||
| 5cae61eb5a | |||
| ba8e0681d5 | |||
| de123a8695 | |||
| 21e5962f98 | |||
| db60e649b8 | |||
| e561788809 | |||
| 3cd6ef4464 | |||
| 39dc636b02 | |||
| 4f03b7193e | |||
| 0d921cd21d | |||
| 1a7e46368e | |||
| 8c8ad02a6f | |||
| 8f070f2190 | |||
| c7641bb1ce | |||
| 97bb338e7d | |||
| ceec00e172 | |||
| d81444683b | |||
| 3627c1e720 | |||
| ba5d8c1b29 | |||
| 26e50bb084 | |||
| b351ab00a2 | |||
| 059d605a6d | |||
| f8a4205ac4 | |||
| 25dd79fba7 | |||
| 0c8acd402a | |||
| 3e876e173a | |||
| ae9c4244d6 | |||
| 3c7d6739b5 | |||
| 560195f9f4 | |||
| 9ce7464c79 | |||
| f980d5d3be | |||
| 96122692cb | |||
| b9c300d570 | |||
| dfcc0f8863 | |||
| f56ce9d3b1 | |||
| bd25240123 | |||
| 0020aa8f59 | |||
| e70e4fa41d | |||
| 881a9a1a08 | |||
| a74e12809b | |||
| eaddd4a132 | |||
| 0c41d0bf51 | |||
| 90e281c8da | |||
| 90d638fba3 | |||
| f5e9b02565 | |||
| de15e5b449 | |||
| b6c7581a31 | |||
| af8ffa0654 | |||
| 4d79b4a766 | |||
| af21dc7df8 | |||
| dc3f992e6e | |||
| 13a9359191 | |||
| f07f9ee7a3 | |||
| 7396230223 | |||
| ed14f58697 | |||
| 37714cb44e | |||
| a5ce17009f | |||
| b565a51ed9 | |||
| 6ca066983d | |||
| 40e040ca1a | |||
| 4289cb2634 | |||
| c966e281d4 | |||
| abb84f1c38 | |||
| e507675860 | |||
| 3445469385 | |||
| 3193e8a712 | |||
| e8af6a6b3b | |||
| 7f4fe4d064 | |||
| a1f990584b | |||
| 54e51be665 | |||
| 0fea760143 | |||
| c3eff6abdc | |||
| c661d5c43a | |||
| b665eaa015 | |||
| b08665e598 | |||
| 883d757392 | |||
| 25df902ec4 | |||
| 5956dd79df | |||
| 70556d9386 | |||
| 9fa50774b4 | |||
| 731414a44f | |||
| d42d08aa57 | |||
| 987b5f4bf4 | |||
| 665978a602 | |||
| 8baa864c35 | |||
| 53a22aa41b | |||
| cf4d7afb9c | |||
| e6b5923ff1 | |||
| 538093855b | |||
| e6b8cbe657 | |||
| af7d5e60b4 | |||
| dbceb3067e | |||
| 425457cb16 | |||
| e5bd18132c | |||
| 2f33867d07 | |||
| fd71c56f16 | |||
| e3c2116501 | |||
| fb17339d89 | |||
| 9fd196642d | |||
| 98897a5379 | |||
| 5542329554 | |||
| 79332c0e5e | |||
| 50a55513d4 | |||
| 3bccdd6c9a | |||
| 76af80e332 | |||
| 7a880ae60c | |||
| 5bc0f9513b | |||
| b77801ece9 | |||
| 7de92c598f | |||
| 693080aa12 | |||
| 25c388d0db | |||
| b1722c8af9 | |||
| b65a5fcd97 | |||
| 1c3cba281a | |||
| 800954f8ce | |||
| f66a3c49c4 | |||
| ef396ac84e | |||
| 7e7b27fdec | |||
| 9c90c1c455 | |||
| b1df52b8ff | |||
| e527b7c5f1 | |||
| 149b9d4c0f | |||
| ef28a63ad3 | |||
| e78558bc06 | |||
| f63d7c4121 | |||
| ef062fb397 | |||
| a2ea7ca039 | |||
| 6876cd787b | |||
| 50a6892c3a | |||
| 1bcc7f78c7 | |||
| 2fd5b76ac1 | |||
| 62f42b3f24 | |||
| 2c58b424a1 | |||
| 381c518b23 | |||
| ebf741114d | |||
| 648dde5e96 | |||
| a3042e6332 | |||
| e5fd3133f4 | |||
| e1bbe57f9c | |||
| d4783e8c14 | |||
| 736880e046 | |||
| bd7a9b5fcf | |||
| 9a47bb2f80 | |||
| d7ad2baf79 | |||
| a951cc996b | |||
| 173e0d6f35 | |||
| 62bb830338 | |||
| f7c6270f74 | |||
| 711fe6ba2c | |||
| fbedb60371 | |||
| 974d2f1627 | |||
| ed401728eb | |||
| fc389a54c5 | |||
| c8b372dba0 | |||
| 2333d75c56 | |||
| 2ef9a8a769 | |||
| 21ab9b9d8c | |||
| 79c1473378 | |||
| 93b8a74351 | |||
| 28185170b0 | |||
| 178883b4cc | |||
| e9f9041b25 | |||
| 175290fa04 | |||
| b0c4d8c541 | |||
| 0f643bca76 | |||
| eeebedcfe8 | |||
| 2f682780fa | |||
| ed83f5369e | |||
| 4ee1bd5f32 | |||
| 1c2bbed405 | |||
| d573fc0e65 | |||
| f8b249e649 | |||
| fbcab757d5 | |||
| c0e998ef6e | |||
| 84f25807db | |||
| 83b242be7b | |||
| a12d740a5d | |||
| 3bbb014dc7 | |||
| f040733e28 | |||
| b0bf7ca486 | |||
| 14d83c8bac | |||
| 8b506dfa42 | |||
| ac2258c2dc | |||
| 3c279edcf2 | |||
| 9ed8a5ed73 | |||
| 3d4ddf4a6f | |||
| 4e0273bb28 | |||
| 7056d2ae99 | |||
| d8fbc00cb9 | |||
| 57c5f0ec87 | |||
| e5bd80c719 | |||
| 25a33a454c | |||
| bd30784b1d | |||
| 28fce0a890 | |||
| e1eb582bea | |||
| 2042ee453b | |||
| 33c4e512f1 | |||
| 253e8a3f98 | |||
| 06b63d65d1 | |||
| 08f3133414 | |||
| d412cddf39 | |||
| 671c5cdd84 | |||
| 554f060092 | |||
| e243e8d8a3 | |||
| 1b935a367f | |||
| 2edd083a71 | |||
| dd50a68bf2 | |||
| e8dd3461e8 | |||
| 8dd4473432 | |||
| b5bbbdd840 | |||
| f0266e13c5 | |||
| ae898652b2 | |||
| c34f67495c | |||
| 815c536e05 | |||
| fc64427ae1 | |||
| 11c518478e | |||
| e823635ce1 | |||
| 98e74c8fde | |||
| 29bfa33d59 | |||
| 3ead0beeb1 | |||
| 2108c44c8b | |||
| b0079e55b4 | |||
| d9f54f8bd7 | |||
| 5a446f8200 | |||
| f4d5e2f43d | |||
| 9121f24181 | |||
| 7dd507af04 | |||
| 3b9aad2ba7 | |||
| ea9f74b581 | |||
| e37aaa482d | |||
| a3170f744c | |||
| ced3780787 | |||
| 6faf26683c | |||
| 8ac9cbf733 | |||
| 098ed34469 | |||
| 6cf4d1002f | |||
| a111d56ea3 | |||
| 8436470fcb | |||
| 17da0e4146 | |||
| ea41e9ab4e | |||
| 5770b5feef | |||
| b5259a3a85 | |||
| 596559efc9 | |||
| b7b03f8594 | |||
| 61ef255809 | |||
| 08426376ac | |||
| d0262c899e | |||
| 152433d88a | |||
| dece58d1a5 | |||
| 70be474aac | |||
| a852cbe7f2 | |||
| 7df38d35c1 | |||
| ef29a5ee3d | |||
| 9a7fe7ef16 | |||
| 8c4ea5c898 | |||
| d06bc2f2e1 | |||
| 534fea7104 | |||
| bc2b9eec58 | |||
| 88c38ddeb3 | |||
| 602753b68a | |||
| 44ebfa3bb8 | |||
| 6bacf7f953 | |||
| 095962f13e | |||
| 0862fd74b0 | |||
| e0139f91c8 | |||
| 64920ef648 | |||
| 7ba70869aa | |||
| f67297688f | |||
| 0841b4c663 | |||
| 440602f52a | |||
| 510120410b | |||
| 4ef67fef3a | |||
| 45561bed9d | |||
| 7bd5e80323 | |||
| 7515eee0a8 | |||
| 452067db19 | |||
| 859920a81f | |||
| 34ce3cac70 | |||
| 12814b55d2 | |||
| 50206ae8a7 | |||
| 169184ac9b | |||
| 33bc58c9c2 | |||
| 65d66768c1 | |||
| c960f7ae48 | |||
| d5104a4268 | |||
| 9069c01f9c | |||
| 0ff41a1127 | |||
| 7192af41e4 | |||
| 5ec387b644 | |||
| 4be479fa06 | |||
| e0d69204cd | |||
| f2d6275da4 | |||
| 992ac38d0d | |||
| f962e61315 | |||
| b3aebb71ff | |||
| 98d3bcd079 | |||
| 1703df5c00 | |||
| 674495680d | |||
| 04f5fe5e38 | |||
| 1b7d0bd4e6 | |||
| 66183c1f0a | |||
| 130ad295d0 | |||
| 6612ba69b1 | |||
| 2dc015b360 | |||
| 2eb43b1e1f | |||
| 8633b2f1f7 | |||
| c9f525a3b2 | |||
| e224c77920 | |||
| 28b8215c9b | |||
| 98eedf14dc | |||
| cd3ee5bd5d | |||
| 26e8f1f876 | |||
| af55665ff2 | |||
| bcd738d2e6 | |||
| 488fcd4f83 | |||
| 5d4d60bb95 | |||
| 7f4bf19186 | |||
| 07c573e52f | |||
| 660c7e4a43 | |||
| 5fafac0ca4 | |||
| c41b62f47e | |||
| f42c1b68a4 | |||
| b90fe73c96 | |||
| d19f47b458 | |||
| 86fd94767c | |||
| d826ac7099 | |||
| 40e23ce8dc | |||
| d50f096b14 | |||
| 1117b6e72d | |||
| c5c5c71d15 | |||
| a31c1d2c69 | |||
| 2352269ba9 | |||
| 985e71ebf4 | |||
| 4d57f04a26 | |||
| ab3b305682 | |||
| b8858708be | |||
| 0a6494abfb | |||
| 75b88a5416 | |||
| e143dbce50 | |||
| 8ad131bb3b | |||
| 41eeb1f2e7 | |||
| 02c1bfc3e7 | |||
| d042cbc62e | |||
| 03750b76ac | |||
| 1befd2a602 | |||
| d1e33ba9ea | |||
| 7d793e12c8 | |||
| 1ce6e279f0 | |||
| ec56f4e839 | |||
| d5ababfed0 | |||
| 8225f98565 | |||
| 4c05316a7b | |||
| 66e588c8ca | |||
| 9a51c2f56a | |||
| ee789db443 | |||
| d360929af1 | |||
| 5f53748d07 | |||
| e3cc4b83c8 | |||
| b5acc8e392 | |||
| f5ea61e93e | |||
| a76a8876d1 | |||
| be1f4b34f8 | |||
| c19a822e1b | |||
| 8782787a9e | |||
| 4c6b8f9229 | |||
| 51dcf4ce84 | |||
| 27e484e7f8 | |||
| 9308287fea | |||
| 7ca5b726a2 | |||
| 0bdd1267fb | |||
| 3ea88dfc7f | |||
| 2275c5b1a3 | |||
| 1c7cf44af4 | |||
| 3325392cc5 | |||
| fd2843b0fb | |||
| 1898a3f8a5 | |||
| 9c4f897b9a | |||
| 47b9d48f70 | |||
| ce68f2cdc6 | |||
| a8fa552b3a | |||
| bd257777a0 | |||
| e6715a2dbe | |||
| 8f46c9113c | |||
| 5aa4e23f54 | |||
| 5821511114 | |||
| d6d9b04c41 | |||
| 540289e6c6 | |||
| 1d971d3240 | |||
| 02a9f0abca | |||
| 289f091bf9 | |||
| 1a4eb47e1d | |||
| 4c70bfa8b8 | |||
| 3a4756449a | |||
| 55b7ea04a7 | |||
| ccfc8c6f15 | |||
| 4fb3fab82d | |||
| 3cea0dfb07 | |||
| 0d6db3a3f3 | |||
| 3d5a81bd30 | |||
| 208604a3a8 | |||
| 63bfba0bdb | |||
| 9948a51b14 | |||
| 0e0bb3582f | |||
| 546062d2cd | |||
| aad0b3c157 | |||
| 4d4265f531 | |||
| e138523123 | |||
| a65e1f71b4 | |||
| 909c062ee1 | |||
| f5322e45fc | |||
| 017f09f1e9 | |||
| 0ba66ab155 | |||
| 5cd267d755 | |||
| d30946dabf | |||
| b0e524213e | |||
| b1adb5652e | |||
| c825d5dcf6 | |||
| 2127d5850f | |||
| ae9fcc2969 | |||
| 624db69f12 | |||
| 80a7843f45 | |||
| cb55176612 | |||
| 5aa2524d33 | |||
| 2575a3a3ab | |||
| f8f7b0ec1a | |||
| d2ee486900 | |||
| c44ddd9831 | |||
| e645cbd8f8 | |||
| 485fc2c416 | |||
| f09be969bb | |||
| 597a0b4d9f | |||
| 779cce3c61 | |||
| b5d9a71cf9 | |||
| c2af415450 | |||
| 89ce61cfea | |||
| 05c5327f47 | |||
| 3891c0a255 | |||
| 63b1d0c1ea | |||
| 75ed38fb3d | |||
| 63db9a7a2f | |||
| 19c80f0f0e | |||
| c5a0bde3ec | |||
| 1261e5e5e8 | |||
| e2ecd68556 | |||
| bceb0eee9b | |||
| 173e818a62 | |||
| 84d8940dbf | |||
| 3e995e6a6d | |||
| 459c36f21b | |||
| 72adb5468c | |||
| 1194957fde | |||
| 68bd29eda2 | |||
| f67a811f7f | |||
| b9c122e7f4 | |||
| 396b39dff9 | |||
| ac8bd12609 | |||
| b55bef4438 | |||
| 2f9667de76 | |||
| a7b6307d32 | |||
| 2883ad6764 | |||
| 0feff5b048 | |||
| 0bce6b35b4 | |||
| 89e23456f0 | |||
| a39173c227 | |||
| 12e93d374f | |||
| 922f9242e4 | |||
| 7fc0a791a2 | |||
| 8d37116fec | |||
| 4b500f988d | |||
| 5ad906ea6a | |||
| 5b862a43e0 | |||
| 1e5cd69205 | |||
| 9081c46565 | |||
| 40b252be8c | |||
| ba1357038a | |||
| 46d1f4c338 | |||
| 9c880dd650 | |||
| 01ba0e050f | |||
| ccc4aae94e | |||
| 01242e13d7 | |||
| 938ee27e42 | |||
| a101f72153 | |||
| 40642433d8 | |||
| 8979181d5e | |||
| c17c6b5c35 | |||
| e83a4090ac | |||
| b71b9f80b9 | |||
| ee87289917 | |||
| 5ad8c3e249 | |||
| 8b992513b8 | |||
| eca0cdc7a9 | |||
| 779e6b8e0b | |||
| c2428361c4 | |||
| 68e4d13f36 | |||
| cb9f4bb100 | |||
| 8a398f3105 | |||
| 0f051d5886 | |||
| e85d9a0d72 | |||
| 06dde4f503 | |||
| 83d4176785 | |||
| c94951b2f8 | |||
| a9cf8f6c5d | |||
| 64ddec0d67 | |||
| da3b0caf5e | |||
| 4fedd43af5 | |||
| a263f28e19 | |||
| d53862f135 | |||
| 608958de1c | |||
| 7eb632eb34 | |||
| 33d4fd357c | |||
| e55bd61c17 | |||
| f2fc213d52 | |||
| f814579ed2 | |||
| 71d299d0d3 | |||
| e178451d04 | |||
| 9a6222f245 | |||
| affe5ed30b | |||
| 4cc5401d7e | |||
| 36e840cd87 | |||
| 985b41c40b | |||
| 2e29ac2829 | |||
| dbfb474eab | |||
| d243de26ec | |||
| 894826771a | |||
| a3386da5d6 | |||
| 318a3d0308 | |||
| 5bafb163cc | |||
| 52b1bc5b09 | |||
| 1873b22e96 | |||
| 9a8c853a2e | |||
| e54383d0fe | |||
| 43c48ba4d7 | |||
| 8f9dbf269e | |||
| cb9ee5903a | |||
| cd406d2794 | |||
| 993a301468 | |||
| 399d3f8da5 | |||
| f9d9ad7a38 | |||
| 2d29345f26 | |||
| 725f9e3dc4 | |||
| 4e1d060439 | |||
| 391007d02e | |||
| e41965061c | |||
| 2b9eb06555 | |||
| 31f7752ba9 | |||
| b23ea0397a | |||
| c51cd42cb4 | |||
| 09ee8ea1f5 | |||
| beda78e911 | |||
| 42d7623cc6 | |||
| 4bd388669a | |||
| 324b47507c | |||
| d2baacdd4b | |||
| 57f358a96b | |||
| 19530e880a | |||
| dbdbb098d5 | |||
| 2c8b47ce44 | |||
| cf50d7c7b5 | |||
| d9a0665b2c | |||
| b818cc0766 | |||
| 90f94be2b3 | |||
| 24111facdd | |||
| 424d34a9c0 | |||
| fbd2d31624 | |||
| b54a0dc1e4 | |||
| f27d669f87 | |||
| fcf04629d3 | |||
| 6b0c6d0cde | |||
| 1063e021f2 | |||
| 303f548408 | |||
| cc68f0e640 | |||
| 9b7b432e08 | |||
| 88863609e9 | |||
| adc6c6c13b | |||
| 2de818530b | |||
| 7e4754392d | |||
| 01c857a67a | |||
| 2c2cc72150 | |||
| f7b78b08fd | |||
| f0e6f11c1c | |||
| a19243068b | |||
| 323c51e095 | |||
| bbc3f90928 | |||
| 1344c3b280 | |||
| 5897b28355 | |||
| 15aa8071f8 | |||
| 097095a69b | |||
| daebe26089 | |||
| c58170f5b8 | |||
| 3a7885819d | |||
| 5fc4dfaf7b | |||
| 953bcc33b1 | |||
| bc14ad6a8f | |||
| cc89b57c1f | |||
| 623c8ae803 | |||
| dede190be2 | |||
| a1513f06c3 | |||
| 3c7180bfd5 | |||
| 51f6ca2bed | |||
| ae9a16a397 | |||
| 52a4bea88f | |||
| 1aaba80211 | |||
| 944db46d4f | |||
| 456684dfc3 | |||
| 40fa0f365c | |||
| 2cb71ad443 | |||
| 8a277da278 | |||
| 7dd802201f | |||
| 79b952ea08 | |||
| 397165a524 | |||
| dc3f13991a | |||
| 8ef657531e | |||
| b40a4c27d3 | |||
| f0be15ded8 | |||
| 7fc161f781 | |||
| 2b54d205fe | |||
| 1940d05e64 | |||
| 905288423f | |||
| 62376f507b | |||
| 51c8dad753 | |||
| 540906fb8a | |||
| b642f5c3e5 | |||
| b36b077d42 | |||
| fe9c2b0e4b | |||
| 548cadacff | |||
| a1171877a4 | |||
| f06cc339cc | |||
| 6bf8982559 | |||
| 364d7ebc40 | |||
| 7cc81e9a43 | |||
| 3409c519e2 | |||
| 5851b42af3 | |||
| c5eae67ac9 | |||
| 865ee473ce | |||
| 08e8145975 | |||
| ec0f20de03 | |||
| 40591a7c50 | |||
| 32d394d65b | |||
| 66fab8722c | |||
| 5a8a68cab8 | |||
| 689761bfcb | |||
| 2394e45ec7 | |||
| 01e6a3a9d9 | |||
| 07f4950cb3 | |||
| 368896d84d | |||
| 408f650b0c | |||
| 7c2e1fa3e2 | |||
| 1da66b9a8c | |||
| 4953762f4e | |||
| 97764c4a57 | |||
| 2ea85d3ba2 | |||
| 1f11300175 | |||
| f317db525f | |||
| 3fa0538f72 | |||
| fcfc96ca05 | |||
| 69c2b422de | |||
| 496baa9335 | |||
| e8657cc3de | |||
| e08c06cbc3 | |||
| 8ca54ddf94 | |||
| 3e073404cc | |||
| 0acabf5f73 | |||
| 38285aa1ac | |||
| 5341cd015b | |||
| c32eebf57d | |||
| 554ba6b8f3 | |||
| a69b8c1e96 | |||
| 6f3fcf2276 | |||
| 3df4bba280 | |||
| 7c0d2e1d98 | |||
| a9336b74fd | |||
| 518937b87f | |||
| e6ab9abf19 | |||
| 87a25e326c | |||
| baf7d2c7c0 | |||
| 22dd0aa20c | |||
| 52e7492cbc | |||
| 7e9d00a5a6 | |||
| ff9cf6c7a4 | |||
| 56593f20b0 | |||
| b7b9b003c9 | |||
| 59639ca9b2 | |||
| 66b8c42a25 |
@ -63,7 +63,7 @@ pnpm analyze-component <path> --json
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ❌ Before: Complex state logic in component
|
// ❌ Before: Complex state logic in component
|
||||||
const Configuration: FC = () => {
|
function Configuration() {
|
||||||
const [modelConfig, setModelConfig] = useState<ModelConfig>(...)
|
const [modelConfig, setModelConfig] = useState<ModelConfig>(...)
|
||||||
const [datasetConfigs, setDatasetConfigs] = useState<DatasetConfigs>(...)
|
const [datasetConfigs, setDatasetConfigs] = useState<DatasetConfigs>(...)
|
||||||
const [completionParams, setCompletionParams] = useState<FormValue>({})
|
const [completionParams, setCompletionParams] = useState<FormValue>({})
|
||||||
@ -85,7 +85,7 @@ export const useModelConfig = (appId: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Component becomes cleaner
|
// Component becomes cleaner
|
||||||
const Configuration: FC = () => {
|
function Configuration() {
|
||||||
const { modelConfig, setModelConfig } = useModelConfig(appId)
|
const { modelConfig, setModelConfig } = useModelConfig(appId)
|
||||||
return <div>...</div>
|
return <div>...</div>
|
||||||
}
|
}
|
||||||
@ -189,8 +189,6 @@ const Template = useMemo(() => {
|
|||||||
|
|
||||||
**Dify Convention**:
|
**Dify Convention**:
|
||||||
- This skill is for component decomposition, not query/mutation design.
|
- This skill is for component decomposition, not query/mutation design.
|
||||||
- When refactoring data fetching, follow `web/AGENTS.md`.
|
|
||||||
- Use `frontend-query-mutation` for contracts, query shape, data-fetching wrappers, query/mutation call-site patterns, conditional queries, invalidation, and mutation error handling.
|
|
||||||
- Do not introduce deprecated `useInvalid` / `useReset`.
|
- Do not introduce deprecated `useInvalid` / `useReset`.
|
||||||
- Do not add thin passthrough `useQuery` wrappers during refactoring; only extract a custom hook when it truly orchestrates multiple queries/mutations or shared derived state.
|
- Do not add thin passthrough `useQuery` wrappers during refactoring; only extract a custom hook when it truly orchestrates multiple queries/mutations or shared derived state.
|
||||||
|
|
||||||
@ -367,7 +365,7 @@ For each extraction:
|
|||||||
┌────────────────────────────────────────┐
|
┌────────────────────────────────────────┐
|
||||||
│ 1. Extract code │
|
│ 1. Extract code │
|
||||||
│ 2. Run: pnpm lint:fix │
|
│ 2. Run: pnpm lint:fix │
|
||||||
│ 3. Run: pnpm type-check:tsgo │
|
│ 3. Run: pnpm type-check │
|
||||||
│ 4. Run: pnpm test │
|
│ 4. Run: pnpm test │
|
||||||
│ 5. Test functionality manually │
|
│ 5. Test functionality manually │
|
||||||
│ 6. PASS? → Next extraction │
|
│ 6. PASS? → Next extraction │
|
||||||
|
|||||||
@ -60,8 +60,10 @@ const Template = useMemo(() => {
|
|||||||
**After** (complexity: ~3):
|
**After** (complexity: ~3):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
import type { ComponentType } from 'react'
|
||||||
|
|
||||||
// Define lookup table outside component
|
// Define lookup table outside component
|
||||||
const TEMPLATE_MAP: Record<AppModeEnum, Record<string, FC<TemplateProps>>> = {
|
const TEMPLATE_MAP: Record<AppModeEnum, Record<string, ComponentType<TemplateProps>>> = {
|
||||||
[AppModeEnum.CHAT]: {
|
[AppModeEnum.CHAT]: {
|
||||||
[LanguagesSupported[1]]: TemplateChatZh,
|
[LanguagesSupported[1]]: TemplateChatZh,
|
||||||
[LanguagesSupported[7]]: TemplateChatJa,
|
[LanguagesSupported[7]]: TemplateChatJa,
|
||||||
|
|||||||
@ -65,10 +65,10 @@ interface ConfigurationHeaderProps {
|
|||||||
onPublish: () => void
|
onPublish: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfigurationHeader: FC<ConfigurationHeaderProps> = ({
|
function ConfigurationHeader({
|
||||||
isAdvancedMode,
|
isAdvancedMode,
|
||||||
onPublish,
|
onPublish,
|
||||||
}) => {
|
}: ConfigurationHeaderProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -136,7 +136,7 @@ const AppInfo = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ✅ After: Separate view components
|
// ✅ After: Separate view components
|
||||||
const AppInfoExpanded: FC<AppInfoViewProps> = ({ appDetail, onAction }) => {
|
function AppInfoExpanded({ appDetail, onAction }: AppInfoViewProps) {
|
||||||
return (
|
return (
|
||||||
<div className="expanded">
|
<div className="expanded">
|
||||||
{/* Clean, focused expanded view */}
|
{/* Clean, focused expanded view */}
|
||||||
@ -144,7 +144,7 @@ const AppInfoExpanded: FC<AppInfoViewProps> = ({ appDetail, onAction }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppInfoCollapsed: FC<AppInfoViewProps> = ({ appDetail, onAction }) => {
|
function AppInfoCollapsed({ appDetail, onAction }: AppInfoViewProps) {
|
||||||
return (
|
return (
|
||||||
<div className="collapsed">
|
<div className="collapsed">
|
||||||
{/* Clean, focused collapsed view */}
|
{/* Clean, focused collapsed view */}
|
||||||
@ -203,12 +203,12 @@ interface AppInfoModalsProps {
|
|||||||
onSuccess: () => void
|
onSuccess: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppInfoModals: FC<AppInfoModalsProps> = ({
|
function AppInfoModals({
|
||||||
appDetail,
|
appDetail,
|
||||||
activeModal,
|
activeModal,
|
||||||
onClose,
|
onClose,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}) => {
|
}: AppInfoModalsProps) {
|
||||||
const handleEdit = async (data) => { /* logic */ }
|
const handleEdit = async (data) => { /* logic */ }
|
||||||
const handleDuplicate = async (data) => { /* logic */ }
|
const handleDuplicate = async (data) => { /* logic */ }
|
||||||
const handleDelete = async () => { /* logic */ }
|
const handleDelete = async () => { /* logic */ }
|
||||||
@ -296,7 +296,7 @@ interface OperationItemProps {
|
|||||||
onAction: (id: string) => void
|
onAction: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const OperationItem: FC<OperationItemProps> = ({ operation, onAction }) => {
|
function OperationItem({ operation, onAction }: OperationItemProps) {
|
||||||
return (
|
return (
|
||||||
<div className="operation-item">
|
<div className="operation-item">
|
||||||
<span className="icon">{operation.icon}</span>
|
<span className="icon">{operation.icon}</span>
|
||||||
@ -435,7 +435,7 @@ interface ChildProps {
|
|||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Child: FC<ChildProps> = ({ value, onChange, onSubmit }) => {
|
function Child({ value, onChange, onSubmit }: ChildProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<input value={value} onChange={e => onChange(e.target.value)} />
|
<input value={value} onChange={e => onChange(e.target.value)} />
|
||||||
|
|||||||
@ -112,13 +112,13 @@ export const useModelConfig = ({
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Before: 50+ lines of state management
|
// Before: 50+ lines of state management
|
||||||
const Configuration: FC = () => {
|
function Configuration() {
|
||||||
const [modelConfig, setModelConfig] = useState<ModelConfig>(...)
|
const [modelConfig, setModelConfig] = useState<ModelConfig>(...)
|
||||||
// ... lots of related state and effects
|
// ... lots of related state and effects
|
||||||
}
|
}
|
||||||
|
|
||||||
// After: Clean component
|
// After: Clean component
|
||||||
const Configuration: FC = () => {
|
function Configuration() {
|
||||||
const {
|
const {
|
||||||
modelConfig,
|
modelConfig,
|
||||||
setModelConfig,
|
setModelConfig,
|
||||||
@ -159,8 +159,6 @@ const Configuration: FC = () => {
|
|||||||
|
|
||||||
When hook extraction touches query or mutation code, do not use this reference as the source of truth for data-layer patterns.
|
When hook extraction touches query or mutation code, do not use this reference as the source of truth for data-layer patterns.
|
||||||
|
|
||||||
- Follow `web/AGENTS.md` first.
|
|
||||||
- Use `frontend-query-mutation` for contracts, query shape, data-fetching wrappers, query/mutation call-site patterns, conditional queries, invalidation, and mutation error handling.
|
|
||||||
- Do not introduce deprecated `useInvalid` / `useReset`.
|
- Do not introduce deprecated `useInvalid` / `useReset`.
|
||||||
- Do not extract thin passthrough `useQuery` hooks; only extract orchestration hooks.
|
- Do not extract thin passthrough `useQuery` hooks; only extract orchestration hooks.
|
||||||
|
|
||||||
|
|||||||
79
.agents/skills/e2e-cucumber-playwright/SKILL.md
Normal file
79
.agents/skills/e2e-cucumber-playwright/SKILL.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
name: e2e-cucumber-playwright
|
||||||
|
description: Write, update, or review Dify end-to-end tests under `e2e/` that use Cucumber, Gherkin, and Playwright. Use when the task involves `.feature` files, `features/step-definitions/`, `features/support/`, `DifyWorld`, scenario tags, locator/assertion choices, or E2E testing best practices for this repository.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Dify E2E Cucumber + Playwright
|
||||||
|
|
||||||
|
Use this skill for Dify's repository-level E2E suite in `e2e/`. Use [`e2e/AGENTS.md`](../../../e2e/AGENTS.md) as the canonical guide for local architecture and conventions, then apply Playwright/Cucumber best practices only where they fit the current suite.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Use this skill for `.feature` files, Cucumber step definitions, `DifyWorld`, hooks, tags, and E2E review work under `e2e/`.
|
||||||
|
- Do not use this skill for Vitest or React Testing Library work under `web/`; use `frontend-testing` instead.
|
||||||
|
- Do not use this skill for backend test or API review tasks under `api/`.
|
||||||
|
|
||||||
|
## Read Order
|
||||||
|
|
||||||
|
1. Read [`e2e/AGENTS.md`](../../../e2e/AGENTS.md) first.
|
||||||
|
2. Read only the files directly involved in the task:
|
||||||
|
- target `.feature` files under `e2e/features/`
|
||||||
|
- related step files under `e2e/features/step-definitions/`
|
||||||
|
- `e2e/features/support/hooks.ts` and `e2e/features/support/world.ts` when session lifecycle or shared state matters
|
||||||
|
- `e2e/scripts/run-cucumber.ts` and `e2e/cucumber.config.ts` when tags or execution flow matter
|
||||||
|
3. Read [`references/playwright-best-practices.md`](references/playwright-best-practices.md) only when locator, assertion, isolation, or waiting choices are involved.
|
||||||
|
4. Read [`references/cucumber-best-practices.md`](references/cucumber-best-practices.md) only when scenario wording, step granularity, tags, or expression design are involved.
|
||||||
|
5. Re-check official Playwright or Cucumber docs with the available documentation tools before introducing a new framework pattern.
|
||||||
|
|
||||||
|
## Local Rules
|
||||||
|
|
||||||
|
- `e2e/` uses Cucumber for scenarios and Playwright as the browser layer.
|
||||||
|
- `DifyWorld` is the per-scenario context object. Type `this` as `DifyWorld` and use `async function`, not arrow functions.
|
||||||
|
- Keep glue organized by capability under `e2e/features/step-definitions/`; use `common/` only for broadly reusable steps.
|
||||||
|
- Browser session behavior comes from `features/support/hooks.ts`:
|
||||||
|
- default: authenticated session with shared storage state
|
||||||
|
- `@unauthenticated`: clean browser context
|
||||||
|
- `@authenticated`: readability/selective-run tag only unless implementation changes
|
||||||
|
- `@fresh`: only for `e2e:full*` flows
|
||||||
|
- Do not import Playwright Test runner patterns that bypass the current Cucumber + `DifyWorld` architecture unless the task is explicitly about changing that architecture.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Rebuild local context.
|
||||||
|
- Inspect the target feature area.
|
||||||
|
- Reuse an existing step when wording and behavior already match.
|
||||||
|
- Add a new step only for a genuinely new user action or assertion.
|
||||||
|
- Keep edits close to the current capability folder unless the step is broadly reusable.
|
||||||
|
2. Write behavior-first scenarios.
|
||||||
|
- Describe user-observable behavior, not DOM mechanics.
|
||||||
|
- Keep each scenario focused on one workflow or outcome.
|
||||||
|
- Keep scenarios independent and re-runnable.
|
||||||
|
3. Write step definitions in the local style.
|
||||||
|
- Keep one step to one user-visible action or one assertion.
|
||||||
|
- Prefer Cucumber Expressions such as `{string}` and `{int}`.
|
||||||
|
- Scope locators to stable containers when the page has repeated elements.
|
||||||
|
- Avoid page-object layers or extra helper abstractions unless repeated complexity clearly justifies them.
|
||||||
|
4. Use Playwright in the local style.
|
||||||
|
- Prefer user-facing locators: `getByRole`, `getByLabel`, `getByPlaceholder`, `getByText`, then `getByTestId` for explicit contracts.
|
||||||
|
- Use web-first `expect(...)` assertions.
|
||||||
|
- Do not use `waitForTimeout`, manual polling, or raw visibility checks when a locator action or retrying assertion already expresses the behavior.
|
||||||
|
5. Validate narrowly.
|
||||||
|
- Run the narrowest tagged scenario or flow that exercises the change.
|
||||||
|
- Run `pnpm -C e2e check`.
|
||||||
|
- Broaden verification only when the change affects hooks, tags, setup, or shared step semantics.
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
|
||||||
|
- Does the scenario describe behavior rather than implementation?
|
||||||
|
- Does it fit the current session model, tags, and `DifyWorld` usage?
|
||||||
|
- Should an existing step be reused instead of adding a new one?
|
||||||
|
- Are locators user-facing and assertions web-first?
|
||||||
|
- Does the change introduce hidden coupling across scenarios, tags, or instance state?
|
||||||
|
- Does it document or implement behavior that differs from the real hooks or configuration?
|
||||||
|
|
||||||
|
Lead findings with correctness, flake risk, and architecture drift.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [`references/playwright-best-practices.md`](references/playwright-best-practices.md)
|
||||||
|
- [`references/cucumber-best-practices.md`](references/cucumber-best-practices.md)
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "E2E Cucumber + Playwright"
|
||||||
|
short_description: "Write and review Dify E2E scenarios."
|
||||||
|
default_prompt: "Use $e2e-cucumber-playwright to write or review a Dify E2E scenario under e2e/."
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
# Cucumber Best Practices For Dify E2E
|
||||||
|
|
||||||
|
Use this reference when writing or reviewing Gherkin scenarios, step definitions, parameter expressions, and step reuse in Dify's `e2e/` suite.
|
||||||
|
|
||||||
|
Official sources:
|
||||||
|
|
||||||
|
- https://cucumber.io/docs/guides/10-minute-tutorial/
|
||||||
|
- https://cucumber.io/docs/cucumber/step-definitions/
|
||||||
|
- https://cucumber.io/docs/cucumber/cucumber-expressions/
|
||||||
|
|
||||||
|
## What Matters Most
|
||||||
|
|
||||||
|
### 1. Treat scenarios as executable specifications
|
||||||
|
|
||||||
|
Cucumber scenarios should describe examples of behavior, not test implementation recipes.
|
||||||
|
|
||||||
|
Apply it like this:
|
||||||
|
|
||||||
|
- write what the user does and what should happen
|
||||||
|
- avoid UI-internal wording such as selector details, DOM structure, or component names
|
||||||
|
- keep language concrete enough that the scenario reads like living documentation
|
||||||
|
|
||||||
|
### 2. Keep scenarios focused
|
||||||
|
|
||||||
|
A scenario should usually prove one workflow or business outcome. If a scenario wanders across several unrelated behaviors, split it.
|
||||||
|
|
||||||
|
In Dify's suite, this means:
|
||||||
|
|
||||||
|
- one capability-focused scenario per feature path
|
||||||
|
- no long setup chains when existing bootstrap or reusable steps already cover them
|
||||||
|
- no hidden dependency on another scenario's side effects
|
||||||
|
|
||||||
|
### 3. Reuse steps, but only when behavior really matches
|
||||||
|
|
||||||
|
Good reuse reduces duplication. Bad reuse hides meaning.
|
||||||
|
|
||||||
|
Prefer reuse when:
|
||||||
|
|
||||||
|
- the user action is genuinely the same
|
||||||
|
- the expected outcome is genuinely the same
|
||||||
|
- the wording stays natural across features
|
||||||
|
|
||||||
|
Write a new step when:
|
||||||
|
|
||||||
|
- the behavior is materially different
|
||||||
|
- reusing the old wording would make the scenario misleading
|
||||||
|
- a supposedly generic step would become an implementation-detail wrapper
|
||||||
|
|
||||||
|
### 4. Prefer Cucumber Expressions
|
||||||
|
|
||||||
|
Use Cucumber Expressions for parameters unless regex is clearly necessary.
|
||||||
|
|
||||||
|
Common examples:
|
||||||
|
|
||||||
|
- `{string}` for labels, names, and visible text
|
||||||
|
- `{int}` for counts
|
||||||
|
- `{float}` for decimal values
|
||||||
|
- `{word}` only when the value is truly a single token
|
||||||
|
|
||||||
|
Keep expressions readable. If a step needs complicated parsing logic, first ask whether the scenario wording should be simpler.
|
||||||
|
|
||||||
|
### 5. Keep step definitions thin and meaningful
|
||||||
|
|
||||||
|
Step definitions are glue between Gherkin and automation, not a second abstraction language.
|
||||||
|
|
||||||
|
For Dify:
|
||||||
|
|
||||||
|
- type `this` as `DifyWorld`
|
||||||
|
- use `async function`
|
||||||
|
- keep each step to one user-visible action or assertion
|
||||||
|
- rely on `DifyWorld` and existing support code for shared context
|
||||||
|
- avoid leaking cross-scenario state
|
||||||
|
|
||||||
|
### 6. Use tags intentionally
|
||||||
|
|
||||||
|
Tags should communicate run scope or session semantics, not become ad hoc metadata.
|
||||||
|
|
||||||
|
In Dify's current suite:
|
||||||
|
|
||||||
|
- capability tags group related scenarios
|
||||||
|
- `@unauthenticated` changes session behavior
|
||||||
|
- `@authenticated` is descriptive/selective, not a behavior switch by itself
|
||||||
|
- `@fresh` belongs to reset/full-install flows only
|
||||||
|
|
||||||
|
If a proposed tag implies behavior, verify that hooks or runner configuration actually implement it.
|
||||||
|
|
||||||
|
## Review Questions
|
||||||
|
|
||||||
|
- Does the scenario read like a real example of product behavior?
|
||||||
|
- Are the steps behavior-oriented instead of implementation-oriented?
|
||||||
|
- Is a reused step still truthful in this feature?
|
||||||
|
- Is a new tag documenting real behavior, or inventing semantics that the suite does not implement?
|
||||||
|
- Would a new reader understand the outcome without opening the step-definition file?
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
# Playwright Best Practices For Dify E2E
|
||||||
|
|
||||||
|
Use this reference when writing or reviewing locator, assertion, isolation, or synchronization logic for Dify's Cucumber-based E2E suite.
|
||||||
|
|
||||||
|
Official sources:
|
||||||
|
|
||||||
|
- https://playwright.dev/docs/best-practices
|
||||||
|
- https://playwright.dev/docs/locators
|
||||||
|
- https://playwright.dev/docs/test-assertions
|
||||||
|
- https://playwright.dev/docs/browser-contexts
|
||||||
|
|
||||||
|
## What Matters Most
|
||||||
|
|
||||||
|
### 1. Keep scenarios isolated
|
||||||
|
|
||||||
|
Playwright's model is built around clean browser contexts so one test does not leak into another. In Dify's suite, that principle maps to per-scenario session setup in `features/support/hooks.ts` and `DifyWorld`.
|
||||||
|
|
||||||
|
Apply it like this:
|
||||||
|
|
||||||
|
- do not depend on another scenario having run first
|
||||||
|
- do not persist ad hoc scenario state outside `DifyWorld`
|
||||||
|
- do not couple ordinary scenarios to `@fresh` behavior
|
||||||
|
- when a flow needs special auth/session semantics, express that through the existing tag model or explicit hook changes
|
||||||
|
|
||||||
|
### 2. Prefer user-facing locators
|
||||||
|
|
||||||
|
Playwright recommends built-in locators that reflect what users perceive on the page.
|
||||||
|
|
||||||
|
Preferred order in this repository:
|
||||||
|
|
||||||
|
1. `getByRole`
|
||||||
|
2. `getByLabel`
|
||||||
|
3. `getByPlaceholder`
|
||||||
|
4. `getByText`
|
||||||
|
5. `getByTestId` when an explicit test contract is the most stable option
|
||||||
|
|
||||||
|
Avoid raw CSS/XPath selectors unless no stable user-facing contract exists and adding one is not practical.
|
||||||
|
|
||||||
|
Also remember:
|
||||||
|
|
||||||
|
- repeated content usually needs scoping to a stable container
|
||||||
|
- exact text matching is often too brittle when role/name or label already exists
|
||||||
|
- `getByTestId` is acceptable when semantics are weak but the contract is intentional
|
||||||
|
|
||||||
|
### 3. Use web-first assertions
|
||||||
|
|
||||||
|
Playwright assertions auto-wait and retry. Prefer them over manual state inspection.
|
||||||
|
|
||||||
|
Prefer:
|
||||||
|
|
||||||
|
- `await expect(page).toHaveURL(...)`
|
||||||
|
- `await expect(locator).toBeVisible()`
|
||||||
|
- `await expect(locator).toBeHidden()`
|
||||||
|
- `await expect(locator).toBeEnabled()`
|
||||||
|
- `await expect(locator).toHaveText(...)`
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
- `expect(await locator.isVisible()).toBe(true)`
|
||||||
|
- custom polling loops for DOM state
|
||||||
|
- `waitForTimeout` as synchronization
|
||||||
|
|
||||||
|
If a condition genuinely needs custom retry logic, use Playwright's polling/assertion tools deliberately and keep that choice local and explicit.
|
||||||
|
|
||||||
|
### 4. Let actions wait for actionability
|
||||||
|
|
||||||
|
Locator actions already wait for the element to be actionable. Do not preface every click/fill with extra timing logic unless the action needs a specific visible/ready assertion for clarity.
|
||||||
|
|
||||||
|
Good pattern:
|
||||||
|
|
||||||
|
- assert a meaningful visible state when that is part of the behavior
|
||||||
|
- then click/fill/select via locator APIs
|
||||||
|
|
||||||
|
Bad pattern:
|
||||||
|
|
||||||
|
- stack arbitrary waits before every action
|
||||||
|
- wait on unstable implementation details instead of the visible state the user cares about
|
||||||
|
|
||||||
|
### 5. Match debugging to the current suite
|
||||||
|
|
||||||
|
Playwright's wider ecosystem supports traces and rich debugging tools. Dify's current suite already captures:
|
||||||
|
|
||||||
|
- full-page screenshots
|
||||||
|
- page HTML
|
||||||
|
- console errors
|
||||||
|
- page errors
|
||||||
|
|
||||||
|
Use the existing artifact flow by default. If a task is specifically about improving diagnostics, confirm the change fits the current Cucumber architecture before importing broader Playwright tooling.
|
||||||
|
|
||||||
|
## Review Questions
|
||||||
|
|
||||||
|
- Would this locator survive DOM refactors that do not change user-visible behavior?
|
||||||
|
- Is this assertion using Playwright's retrying semantics?
|
||||||
|
- Is any explicit wait masking a real readiness problem?
|
||||||
|
- Does this code preserve per-scenario isolation?
|
||||||
|
- Is a new abstraction really needed, or does it bypass the existing `DifyWorld` + step-definition model?
|
||||||
@ -9,18 +9,18 @@ Category: Performance
|
|||||||
|
|
||||||
When rendering React Flow, prefer `useNodes`/`useEdges` for UI consumption and rely on `useStoreApi` inside callbacks that mutate or read node/edge state. Avoid manually pulling Flow data outside of these hooks.
|
When rendering React Flow, prefer `useNodes`/`useEdges` for UI consumption and rely on `useStoreApi` inside callbacks that mutate or read node/edge state. Avoid manually pulling Flow data outside of these hooks.
|
||||||
|
|
||||||
## Complex prop memoization
|
## Complex prop stability
|
||||||
|
|
||||||
IsUrgent: True
|
IsUrgent: False
|
||||||
Category: Performance
|
Category: Performance
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
|
|
||||||
Wrap complex prop values (objects, arrays, maps) in `useMemo` prior to passing them into child components to guarantee stable references and prevent unnecessary renders.
|
Only require stable object, array, or map props when there is a clear reason: the child is memoized, the value participates in effect/query dependencies, the value is part of a stable-reference API contract, or profiling/local behavior shows avoidable re-renders. Do not request `useMemo` for every inline object by default; `how-to-write-component` treats memoization as a targeted optimization.
|
||||||
|
|
||||||
Update this file when adding, editing, or removing Performance rules so the catalog remains accurate.
|
Update this file when adding, editing, or removing Performance rules so the catalog remains accurate.
|
||||||
|
|
||||||
Wrong:
|
Risky:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<HeavyComp
|
<HeavyComp
|
||||||
@ -31,7 +31,7 @@ Wrong:
|
|||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
Right:
|
Better when stable identity matters:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const config = useMemo(() => ({
|
const config = useMemo(() => ({
|
||||||
|
|||||||
@ -1,44 +0,0 @@
|
|||||||
---
|
|
||||||
name: frontend-query-mutation
|
|
||||||
description: Guide for implementing Dify frontend query and mutation patterns with TanStack Query and oRPC. Trigger when creating or updating contracts in web/contract, wiring router composition, consuming consoleQuery or marketplaceQuery in components or services, deciding whether to call queryOptions() directly or extract a helper or use-* hook, handling conditional queries, cache invalidation, mutation error handling, or migrating legacy service calls to contract-first query and mutation helpers.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Frontend Query & Mutation
|
|
||||||
|
|
||||||
## Intent
|
|
||||||
|
|
||||||
- Keep contract as the single source of truth in `web/contract/*`.
|
|
||||||
- Prefer contract-shaped `queryOptions()` and `mutationOptions()`.
|
|
||||||
- Keep invalidation and mutation flow knowledge in the service layer.
|
|
||||||
- Keep abstractions minimal to preserve TypeScript inference.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. Identify the change surface.
|
|
||||||
- Read `references/contract-patterns.md` for contract files, router composition, client helpers, and query or mutation call-site shape.
|
|
||||||
- Read `references/runtime-rules.md` for conditional queries, invalidation, error handling, and legacy migrations.
|
|
||||||
- Read both references when a task spans contract shape and runtime behavior.
|
|
||||||
2. Implement the smallest abstraction that fits the task.
|
|
||||||
- Default to direct `useQuery(...)` or `useMutation(...)` calls with oRPC helpers at the call site.
|
|
||||||
- Extract a small shared query helper only when multiple call sites share the same extra options.
|
|
||||||
- Create `web/service/use-{domain}.ts` only for orchestration or shared domain behavior.
|
|
||||||
3. Preserve Dify conventions.
|
|
||||||
- Keep contract inputs in `{ params, query?, body? }` shape.
|
|
||||||
- Bind invalidation in the service-layer mutation definition.
|
|
||||||
- Prefer `mutate(...)`; use `mutateAsync(...)` only when Promise semantics are required.
|
|
||||||
|
|
||||||
## Files Commonly Touched
|
|
||||||
|
|
||||||
- `web/contract/console/*.ts`
|
|
||||||
- `web/contract/marketplace.ts`
|
|
||||||
- `web/contract/router.ts`
|
|
||||||
- `web/service/client.ts`
|
|
||||||
- `web/service/use-*.ts`
|
|
||||||
- component and hook call sites using `consoleQuery` or `marketplaceQuery`
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- Use `references/contract-patterns.md` for contract shape, router registration, query and mutation helpers, and anti-patterns that degrade inference.
|
|
||||||
- Use `references/runtime-rules.md` for conditional queries, invalidation, `mutate` versus `mutateAsync`, and legacy migration rules.
|
|
||||||
|
|
||||||
Treat this skill as the single query and mutation entry point for Dify frontend work. Keep detailed rules in the reference files instead of duplicating them in project docs.
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
interface:
|
|
||||||
display_name: "Frontend Query & Mutation"
|
|
||||||
short_description: "Dify TanStack Query and oRPC patterns"
|
|
||||||
default_prompt: "Use this skill when implementing or reviewing Dify frontend contracts, query and mutation call sites, conditional queries, invalidation, or legacy query/mutation migrations."
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
# Contract Patterns
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- Intent
|
|
||||||
- Minimal structure
|
|
||||||
- Core workflow
|
|
||||||
- Query usage decision rule
|
|
||||||
- Mutation usage decision rule
|
|
||||||
- Anti-patterns
|
|
||||||
- Contract rules
|
|
||||||
- Type export
|
|
||||||
|
|
||||||
## Intent
|
|
||||||
|
|
||||||
- Keep contract as the single source of truth in `web/contract/*`.
|
|
||||||
- Default query usage to call-site `useQuery(consoleQuery|marketplaceQuery.xxx.queryOptions(...))` when endpoint behavior maps 1:1 to the contract.
|
|
||||||
- Keep abstractions minimal and preserve TypeScript inference.
|
|
||||||
|
|
||||||
## Minimal Structure
|
|
||||||
|
|
||||||
```text
|
|
||||||
web/contract/
|
|
||||||
├── base.ts
|
|
||||||
├── router.ts
|
|
||||||
├── marketplace.ts
|
|
||||||
└── console/
|
|
||||||
├── billing.ts
|
|
||||||
└── ...other domains
|
|
||||||
web/service/client.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Core Workflow
|
|
||||||
|
|
||||||
1. Define contract in `web/contract/console/{domain}.ts` or `web/contract/marketplace.ts`.
|
|
||||||
- Use `base.route({...}).output(type<...>())` as the baseline.
|
|
||||||
- Add `.input(type<...>())` only when the request has `params`, `query`, or `body`.
|
|
||||||
- For `GET` without input, omit `.input(...)`; do not use `.input(type<unknown>())`.
|
|
||||||
2. Register contract in `web/contract/router.ts`.
|
|
||||||
- Import directly from domain files and nest by API prefix.
|
|
||||||
3. Consume from UI call sites via oRPC query utilities.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import { consoleQuery } from '@/service/client'
|
|
||||||
|
|
||||||
const invoiceQuery = useQuery(consoleQuery.billing.invoices.queryOptions({
|
|
||||||
staleTime: 5 * 60 * 1000,
|
|
||||||
throwOnError: true,
|
|
||||||
select: invoice => invoice.url,
|
|
||||||
}))
|
|
||||||
```
|
|
||||||
|
|
||||||
## Query Usage Decision Rule
|
|
||||||
|
|
||||||
1. Default to direct `*.queryOptions(...)` usage at the call site.
|
|
||||||
2. If 3 or more call sites share the same extra options, extract a small query helper, not a `use-*` passthrough hook.
|
|
||||||
3. Create `web/service/use-{domain}.ts` only for orchestration.
|
|
||||||
- Combine multiple queries or mutations.
|
|
||||||
- Share domain-level derived state or invalidation helpers.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const invoicesBaseQueryOptions = () =>
|
|
||||||
consoleQuery.billing.invoices.queryOptions({ retry: false })
|
|
||||||
|
|
||||||
const invoiceQuery = useQuery({
|
|
||||||
...invoicesBaseQueryOptions(),
|
|
||||||
throwOnError: true,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mutation Usage Decision Rule
|
|
||||||
|
|
||||||
1. Default to mutation helpers from `consoleQuery` or `marketplaceQuery`, for example `useMutation(consoleQuery.billing.bindPartnerStack.mutationOptions(...))`.
|
|
||||||
2. If the mutation flow is heavily custom, use oRPC clients as `mutationFn`, for example `consoleClient.xxx` or `marketplaceClient.xxx`, instead of handwritten non-oRPC mutation logic.
|
|
||||||
|
|
||||||
## Anti-Patterns
|
|
||||||
|
|
||||||
- Do not wrap `useQuery` with `options?: Partial<UseQueryOptions>`.
|
|
||||||
- Do not split local `queryKey` and `queryFn` when oRPC `queryOptions` already exists and fits the use case.
|
|
||||||
- Do not create thin `use-*` passthrough hooks for a single endpoint.
|
|
||||||
- These patterns can degrade inference, especially around `throwOnError` and `select`, and add unnecessary indirection.
|
|
||||||
|
|
||||||
## Contract Rules
|
|
||||||
|
|
||||||
- Input structure: always use `{ params, query?, body? }`.
|
|
||||||
- No-input `GET`: omit `.input(...)`; do not use `.input(type<unknown>())`.
|
|
||||||
- Path params: use `{paramName}` in the path and match it in the `params` object.
|
|
||||||
- Router nesting: group by API prefix, for example `/billing/*` becomes `billing: {}`.
|
|
||||||
- No barrel files: import directly from specific files.
|
|
||||||
- Types: import from `@/types/` and use the `type<T>()` helper.
|
|
||||||
- Mutations: prefer `mutationOptions`; use explicit `mutationKey` mainly for defaults, filtering, and devtools.
|
|
||||||
|
|
||||||
## Type Export
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export type ConsoleInputs = InferContractRouterInputs<typeof consoleRouterContract>
|
|
||||||
```
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
# Runtime Rules
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- Conditional queries
|
|
||||||
- Cache invalidation
|
|
||||||
- Key API guide
|
|
||||||
- `mutate` vs `mutateAsync`
|
|
||||||
- Legacy migration
|
|
||||||
|
|
||||||
## Conditional Queries
|
|
||||||
|
|
||||||
Prefer contract-shaped `queryOptions(...)`.
|
|
||||||
When required input is missing, prefer `input: skipToken` instead of placeholder params or non-null assertions.
|
|
||||||
Use `enabled` only for extra business gating after the input itself is already valid.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { skipToken, useQuery } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
// Disable the query by skipping input construction.
|
|
||||||
function useAccessMode(appId: string | undefined) {
|
|
||||||
return useQuery(consoleQuery.accessControl.appAccessMode.queryOptions({
|
|
||||||
input: appId
|
|
||||||
? { params: { appId } }
|
|
||||||
: skipToken,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid runtime-only guards that bypass type checking.
|
|
||||||
function useBadAccessMode(appId: string | undefined) {
|
|
||||||
return useQuery(consoleQuery.accessControl.appAccessMode.queryOptions({
|
|
||||||
input: { params: { appId: appId! } },
|
|
||||||
enabled: !!appId,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cache Invalidation
|
|
||||||
|
|
||||||
Bind invalidation in the service-layer mutation definition.
|
|
||||||
Components may add UI feedback in call-site callbacks, but they should not decide which queries to invalidate.
|
|
||||||
|
|
||||||
Use:
|
|
||||||
|
|
||||||
- `.key()` for namespace or prefix invalidation
|
|
||||||
- `.queryKey(...)` only for exact cache reads or writes such as `getQueryData` and `setQueryData`
|
|
||||||
- `queryClient.invalidateQueries(...)` in mutation `onSuccess`
|
|
||||||
|
|
||||||
Do not use deprecated `useInvalid` from `use-base.ts`.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Service layer owns cache invalidation.
|
|
||||||
export const useUpdateAccessMode = () => {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation(consoleQuery.accessControl.updateAccessMode.mutationOptions({
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: consoleQuery.accessControl.appWhitelistSubjects.key(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Component only adds UI behavior.
|
|
||||||
updateAccessMode({ appId, mode }, {
|
|
||||||
onSuccess: () => toast.success('...'),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Avoid putting invalidation knowledge in the component.
|
|
||||||
mutate({ appId, mode }, {
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: consoleQuery.accessControl.appWhitelistSubjects.key(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key API Guide
|
|
||||||
|
|
||||||
- `.key(...)`
|
|
||||||
- Use for partial matching operations.
|
|
||||||
- Prefer it for invalidation, refetch, and cancel patterns.
|
|
||||||
- Example: `queryClient.invalidateQueries({ queryKey: consoleQuery.billing.key() })`
|
|
||||||
- `.queryKey(...)`
|
|
||||||
- Use for a specific query's full key.
|
|
||||||
- Prefer it for exact cache addressing and direct reads or writes.
|
|
||||||
- `.mutationKey(...)`
|
|
||||||
- Use for a specific mutation's full key.
|
|
||||||
- Prefer it for mutation defaults registration, mutation-status filtering, and devtools grouping.
|
|
||||||
|
|
||||||
## `mutate` vs `mutateAsync`
|
|
||||||
|
|
||||||
Prefer `mutate` by default.
|
|
||||||
Use `mutateAsync` only when Promise semantics are truly required, such as parallel mutations or sequential steps with result dependencies.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- Event handlers should usually call `mutate(...)` with `onSuccess` or `onError`.
|
|
||||||
- Every `await mutateAsync(...)` must be wrapped in `try/catch`.
|
|
||||||
- Do not use `mutateAsync` when callbacks already express the flow clearly.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Default case.
|
|
||||||
mutation.mutate(data, {
|
|
||||||
onSuccess: result => router.push(result.url),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Promise semantics are required.
|
|
||||||
try {
|
|
||||||
const order = await createOrder.mutateAsync(orderData)
|
|
||||||
await confirmPayment.mutateAsync({ orderId: order.id, token })
|
|
||||||
router.push(`/orders/${order.id}`)
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
toast.error(error instanceof Error ? error.message : 'Unknown error')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Legacy Migration
|
|
||||||
|
|
||||||
When touching old code, migrate it toward these rules:
|
|
||||||
|
|
||||||
| Old pattern | New pattern |
|
|
||||||
|---|---|
|
|
||||||
| `useInvalid(key)` in service layer | `queryClient.invalidateQueries(...)` inside mutation `onSuccess` |
|
|
||||||
| component-triggered invalidation after mutation | move invalidation into the service-layer mutation definition |
|
|
||||||
| imperative fetch plus manual invalidation | wrap it in `useMutation(...mutationOptions(...))` |
|
|
||||||
| `await mutateAsync()` without `try/catch` | switch to `mutate(...)` or add `try/catch` |
|
|
||||||
@ -5,7 +5,7 @@ description: Generate Vitest + React Testing Library tests for Dify frontend com
|
|||||||
|
|
||||||
# Dify Frontend Testing Skill
|
# Dify Frontend Testing Skill
|
||||||
|
|
||||||
This skill enables Claude to generate high-quality, comprehensive frontend tests for the Dify project following established conventions and best practices.
|
This skill enables Codex to generate high-quality, comprehensive frontend tests for the Dify project following established conventions and best practices.
|
||||||
|
|
||||||
> **⚠️ Authoritative Source**: This skill is derived from `web/docs/test.md`. Use Vitest mock/timer APIs (`vi.*`).
|
> **⚠️ Authoritative Source**: This skill is derived from `web/docs/test.md`. Use Vitest mock/timer APIs (`vi.*`).
|
||||||
|
|
||||||
@ -24,35 +24,27 @@ Apply this skill when the user:
|
|||||||
**Do NOT apply** when:
|
**Do NOT apply** when:
|
||||||
|
|
||||||
- User is asking about backend/API tests (Python/pytest)
|
- User is asking about backend/API tests (Python/pytest)
|
||||||
- User is asking about E2E tests (Playwright/Cypress)
|
- User is asking about E2E tests (Cucumber + Playwright under `e2e/`)
|
||||||
- User is only asking conceptual questions without code context
|
- User is only asking conceptual questions without code context
|
||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
### Tech Stack
|
|
||||||
|
|
||||||
| Tool | Version | Purpose |
|
|
||||||
|------|---------|---------|
|
|
||||||
| Vitest | 4.0.16 | Test runner |
|
|
||||||
| React Testing Library | 16.0 | Component testing |
|
|
||||||
| jsdom | - | Test environment |
|
|
||||||
| nock | 14.0 | HTTP mocking |
|
|
||||||
| TypeScript | 5.x | Type safety |
|
|
||||||
|
|
||||||
### Key Commands
|
### Key Commands
|
||||||
|
|
||||||
|
Run these commands from `web/`. From the repository root, prefix them with `pnpm -C web`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Run all tests
|
||||||
pnpm test
|
pnpm test
|
||||||
|
|
||||||
# Watch mode
|
# Watch mode
|
||||||
pnpm test:watch
|
pnpm test --watch
|
||||||
|
|
||||||
# Run specific file
|
# Run specific file
|
||||||
pnpm test path/to/file.spec.tsx
|
pnpm test path/to/file.spec.tsx
|
||||||
|
|
||||||
# Generate coverage report
|
# Generate coverage report
|
||||||
pnpm test:coverage
|
pnpm test --coverage
|
||||||
|
|
||||||
# Analyze component complexity
|
# Analyze component complexity
|
||||||
pnpm analyze-component <path>
|
pnpm analyze-component <path>
|
||||||
@ -200,7 +192,7 @@ When assigned to test a directory/path, test **ALL content** within that path:
|
|||||||
|
|
||||||
- ✅ **Import real project components** directly (including base components and siblings)
|
- ✅ **Import real project components** directly (including base components and siblings)
|
||||||
- ✅ **Only mock**: API services (`@/service/*`), `next/navigation`, complex context providers
|
- ✅ **Only mock**: API services (`@/service/*`), `next/navigation`, complex context providers
|
||||||
- ❌ **DO NOT mock** base components (`@/app/components/base/*`)
|
- ❌ **DO NOT mock** base components (`@/app/components/base/*`) or dify-ui primitives (`@langgenius/dify-ui/*`)
|
||||||
- ❌ **DO NOT mock** sibling/child components in the same directory
|
- ❌ **DO NOT mock** sibling/child components in the same directory
|
||||||
|
|
||||||
> See [Test Structure Template](#test-structure-template) for correct import/mock patterns.
|
> See [Test Structure Template](#test-structure-template) for correct import/mock patterns.
|
||||||
@ -228,7 +220,10 @@ Every test should clearly separate:
|
|||||||
### 2. Black-Box Testing
|
### 2. Black-Box Testing
|
||||||
|
|
||||||
- Test observable behavior, not implementation details
|
- Test observable behavior, not implementation details
|
||||||
- Use semantic queries (getByRole, getByLabelText)
|
- Use semantic queries (`getByRole` with accessible `name`, `getByLabelText`, `getByPlaceholderText`, `getByText`, and scoped `within(...)`)
|
||||||
|
- Treat `getByTestId` as a last resort. If a control cannot be found by role/name, label, landmark, or dialog scope, fix the component accessibility first instead of adding or relying on `data-testid`.
|
||||||
|
- Remove production `data-testid` attributes when semantic selectors can cover the behavior. Keep them only for non-visual mocked boundaries, editor/browser shims such as Monaco, canvas/chart output, or third-party widgets with no accessible DOM in the test environment.
|
||||||
|
- Do not assert decorative icons by test id. Assert the named control that contains them, or mark decorative icons `aria-hidden`.
|
||||||
- Avoid testing internal state directly
|
- Avoid testing internal state directly
|
||||||
- **Prefer pattern matching over hardcoded strings** in assertions:
|
- **Prefer pattern matching over hardcoded strings** in assertions:
|
||||||
|
|
||||||
@ -325,12 +320,12 @@ For more detailed information, refer to:
|
|||||||
### Reference Examples in Codebase
|
### Reference Examples in Codebase
|
||||||
|
|
||||||
- `web/utils/classnames.spec.ts` - Utility function tests
|
- `web/utils/classnames.spec.ts` - Utility function tests
|
||||||
- `web/app/components/base/button/index.spec.tsx` - Component tests
|
- `web/app/components/base/radio/__tests__/index.spec.tsx` - Component tests
|
||||||
- `web/__mocks__/provider-context.ts` - Mock factory example
|
- `web/__mocks__/provider-context.ts` - Mock factory example
|
||||||
|
|
||||||
### Project Configuration
|
### Project Configuration
|
||||||
|
|
||||||
- `web/vitest.config.ts` - Vitest configuration
|
- `web/vite.config.ts` - Vite/Vitest configuration
|
||||||
- `web/vitest.setup.ts` - Test environment setup
|
- `web/vitest.setup.ts` - Test environment setup
|
||||||
- `web/scripts/analyze-component.js` - Component analysis tool
|
- `web/scripts/analyze-component.js` - Component analysis tool
|
||||||
- Modules are not mocked automatically. Global mocks live in `web/vitest.setup.ts` (for example `react-i18next`, `next/image`); mock other modules like `ky` or `mime` locally in test files.
|
- Modules are not mocked automatically. Global mocks live in `web/vitest.setup.ts` (for example `react-i18next`, `next/image`); mock other modules like `ky` or `mime` locally in test files.
|
||||||
|
|||||||
@ -36,7 +36,7 @@ Use this checklist when generating or reviewing tests for Dify frontend componen
|
|||||||
|
|
||||||
### Integration vs Mocking
|
### Integration vs Mocking
|
||||||
|
|
||||||
- [ ] **DO NOT mock base components** (`Loading`, `Button`, `Tooltip`, etc.)
|
- [ ] **DO NOT mock base components or dify-ui primitives** (base `Loading`, `Input`, `Badge`; dify-ui `Button`, `Tooltip`, `Dialog`, etc.)
|
||||||
- [ ] Import real project components instead of mocking
|
- [ ] Import real project components instead of mocking
|
||||||
- [ ] Only mock: API calls, complex context providers, third-party libs with side effects
|
- [ ] Only mock: API calls, complex context providers, third-party libs with side effects
|
||||||
- [ ] Prefer integration testing when using single spec file
|
- [ ] Prefer integration testing when using single spec file
|
||||||
@ -73,7 +73,7 @@ Use this checklist when generating or reviewing tests for Dify frontend componen
|
|||||||
|
|
||||||
### Mocks
|
### Mocks
|
||||||
|
|
||||||
- [ ] **DO NOT mock base components** (`@/app/components/base/*`)
|
- [ ] **DO NOT mock base components or dify-ui primitives** (`@/app/components/base/*` or `@langgenius/dify-ui/*`)
|
||||||
- [ ] `vi.clearAllMocks()` in `beforeEach` (not `afterEach`)
|
- [ ] `vi.clearAllMocks()` in `beforeEach` (not `afterEach`)
|
||||||
- [ ] Shared mock state reset in `beforeEach`
|
- [ ] Shared mock state reset in `beforeEach`
|
||||||
- [ ] i18n uses global mock (auto-loaded in `web/vitest.setup.ts`); only override locally for custom translations
|
- [ ] i18n uses global mock (auto-loaded in `web/vitest.setup.ts`); only override locally for custom translations
|
||||||
@ -127,7 +127,7 @@ For the current file being tested:
|
|||||||
- [ ] Run full directory test: `pnpm test path/to/directory/`
|
- [ ] Run full directory test: `pnpm test path/to/directory/`
|
||||||
- [ ] Check coverage report: `pnpm test:coverage`
|
- [ ] Check coverage report: `pnpm test:coverage`
|
||||||
- [ ] Run `pnpm lint:fix` on all test files
|
- [ ] Run `pnpm lint:fix` on all test files
|
||||||
- [ ] Run `pnpm type-check:tsgo`
|
- [ ] Run `pnpm type-check`
|
||||||
|
|
||||||
## Common Issues to Watch
|
## Common Issues to Watch
|
||||||
|
|
||||||
|
|||||||
@ -2,29 +2,27 @@
|
|||||||
|
|
||||||
## ⚠️ Important: What NOT to Mock
|
## ⚠️ Important: What NOT to Mock
|
||||||
|
|
||||||
### DO NOT Mock Base Components
|
### DO NOT Mock Base Components or dify-ui Primitives
|
||||||
|
|
||||||
**Never mock components from `@/app/components/base/`** such as:
|
**Never mock components from `@/app/components/base/` or from `@langgenius/dify-ui/*`** such as:
|
||||||
|
|
||||||
- `Loading`, `Spinner`
|
- Legacy base (`@/app/components/base/*`): `Loading`, `Spinner`, `Input`, `Badge`, `Tag`
|
||||||
- `Button`, `Input`, `Select`
|
- dify-ui primitives (`@langgenius/dify-ui/*`): `Button`, `Tooltip`, `Dialog`, `Popover`, `DropdownMenu`, `ContextMenu`, `Select`, `AlertDialog`, `Toast`
|
||||||
- `Tooltip`, `Modal`, `Dropdown`
|
|
||||||
- `Icon`, `Badge`, `Tag`
|
|
||||||
|
|
||||||
**Why?**
|
**Why?**
|
||||||
|
|
||||||
- Base components will have their own dedicated tests
|
- These components have their own dedicated tests
|
||||||
- Mocking them creates false positives (tests pass but real integration fails)
|
- Mocking them creates false positives (tests pass but real integration fails)
|
||||||
- Using real components tests actual integration behavior
|
- Using real components tests actual integration behavior
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ❌ WRONG: Don't mock base components
|
// ❌ WRONG: Don't mock base components or dify-ui primitives
|
||||||
vi.mock('@/app/components/base/loading', () => () => <div>Loading</div>)
|
vi.mock('@/app/components/base/loading', () => () => <div>Loading</div>)
|
||||||
vi.mock('@/app/components/base/button', () => ({ children }: any) => <button>{children}</button>)
|
vi.mock('@langgenius/dify-ui/button', () => ({ Button: ({ children }: any) => <button>{children}</button> }))
|
||||||
|
|
||||||
// ✅ CORRECT: Import and use real base components
|
// ✅ CORRECT: Import and use the real components
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import Button from '@/app/components/base/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
// They will render normally in tests
|
// They will render normally in tests
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -58,7 +56,7 @@ See [Zustand Store Testing](#zustand-store-testing) section for full details.
|
|||||||
|
|
||||||
| Location | Purpose |
|
| Location | Purpose |
|
||||||
|----------|---------|
|
|----------|---------|
|
||||||
| `web/vitest.setup.ts` | Global mocks shared by all tests (`react-i18next`, `next/image`, `zustand`) |
|
| `web/vitest.setup.ts` | Global mocks shared by all tests (`react-i18next`, `zustand`, clipboard, FloatingPortal, Monaco, localStorage`) |
|
||||||
| `web/__mocks__/zustand.ts` | Zustand mock implementation (auto-resets stores after each test) |
|
| `web/__mocks__/zustand.ts` | Zustand mock implementation (auto-resets stores after each test) |
|
||||||
| `web/__mocks__/` | Reusable mock factories shared across multiple test files |
|
| `web/__mocks__/` | Reusable mock factories shared across multiple test files |
|
||||||
| Test file | Test-specific mocks, inline with `vi.mock()` |
|
| Test file | Test-specific mocks, inline with `vi.mock()` |
|
||||||
@ -218,28 +216,21 @@ describe('Component', () => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. HTTP Mocking with Nock
|
### 5. HTTP and `fetch` Mocking
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import nock from 'nock'
|
|
||||||
|
|
||||||
const GITHUB_HOST = 'https://api.github.com'
|
|
||||||
const GITHUB_PATH = '/repos/owner/repo'
|
|
||||||
|
|
||||||
const mockGithubApi = (status: number, body: Record<string, unknown>, delayMs = 0) => {
|
|
||||||
return nock(GITHUB_HOST)
|
|
||||||
.get(GITHUB_PATH)
|
|
||||||
.delay(delayMs)
|
|
||||||
.reply(status, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('GithubComponent', () => {
|
describe('GithubComponent', () => {
|
||||||
afterEach(() => {
|
beforeEach(() => {
|
||||||
nock.cleanAll()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should display repo info', async () => {
|
it('should display repo info', async () => {
|
||||||
mockGithubApi(200, { name: 'dify', stars: 1000 })
|
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
||||||
|
new Response(JSON.stringify({ name: 'dify', stars: 1000 }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
render(<GithubComponent />)
|
render(<GithubComponent />)
|
||||||
|
|
||||||
@ -249,7 +240,12 @@ describe('GithubComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle API error', async () => {
|
it('should handle API error', async () => {
|
||||||
mockGithubApi(500, { message: 'Server error' })
|
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
||||||
|
new Response(JSON.stringify({ message: 'Server error' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
render(<GithubComponent />)
|
render(<GithubComponent />)
|
||||||
|
|
||||||
@ -260,6 +256,8 @@ describe('GithubComponent', () => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Prefer mocking `@/service/*` modules or spying on `global.fetch` / `ky` clients with deterministic responses. Do not introduce an HTTP interception dependency such as `nock` or MSW unless it is already declared in the workspace or adding it is part of the task.
|
||||||
|
|
||||||
### 6. Context Providers
|
### 6. Context Providers
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -319,7 +317,7 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
|
|||||||
|
|
||||||
### ✅ DO
|
### ✅ DO
|
||||||
|
|
||||||
1. **Use real base components** - Import from `@/app/components/base/` directly
|
1. **Use real base components and dify-ui primitives** - Import from `@/app/components/base/` or `@langgenius/dify-ui/*` directly
|
||||||
1. **Use real project components** - Prefer importing over mocking
|
1. **Use real project components** - Prefer importing over mocking
|
||||||
1. **Use real Zustand stores** - Set test state via `store.setState()`
|
1. **Use real Zustand stores** - Set test state via `store.setState()`
|
||||||
1. **Reset mocks in `beforeEach`**, not `afterEach`
|
1. **Reset mocks in `beforeEach`**, not `afterEach`
|
||||||
@ -330,11 +328,11 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
|
|||||||
|
|
||||||
### ❌ DON'T
|
### ❌ DON'T
|
||||||
|
|
||||||
1. **Don't mock base components** (`Loading`, `Button`, `Tooltip`, etc.)
|
1. **Don't mock base components or dify-ui primitives** (`Loading`, `Input`, `Button`, `Tooltip`, `Dialog`, etc.)
|
||||||
1. **Don't mock Zustand store modules** - Use real stores with `setState()`
|
1. **Don't mock Zustand store modules** - Use real stores with `setState()`
|
||||||
1. Don't mock components you can import directly
|
1. Don't mock components you can import directly
|
||||||
1. Don't create overly simplified mocks that miss conditional logic
|
1. Don't create overly simplified mocks that miss conditional logic
|
||||||
1. Don't forget to clean up nock after each test
|
1. Don't leave HTTP mocks or service mock state leaking between tests
|
||||||
1. Don't use `any` types in mocks without necessity
|
1. Don't use `any` types in mocks without necessity
|
||||||
|
|
||||||
### Mock Decision Tree
|
### Mock Decision Tree
|
||||||
@ -342,7 +340,7 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
|
|||||||
```
|
```
|
||||||
Need to use a component in test?
|
Need to use a component in test?
|
||||||
│
|
│
|
||||||
├─ Is it from @/app/components/base/*?
|
├─ Is it from @/app/components/base/* or @langgenius/dify-ui/*?
|
||||||
│ └─ YES → Import real component, DO NOT mock
|
│ └─ YES → Import real component, DO NOT mock
|
||||||
│
|
│
|
||||||
├─ Is it a project component?
|
├─ Is it a project component?
|
||||||
|
|||||||
@ -227,12 +227,12 @@ Failing tests compound:
|
|||||||
|
|
||||||
**Fix failures immediately before proceeding.**
|
**Fix failures immediately before proceeding.**
|
||||||
|
|
||||||
## Integration with Claude's Todo Feature
|
## Integration with Codex's Todo Feature
|
||||||
|
|
||||||
When using Claude for multi-file testing:
|
When using Codex for multi-file testing:
|
||||||
|
|
||||||
1. **Ask Claude to create a todo list** before starting
|
1. **Create a todo list** before starting
|
||||||
1. **Request one file at a time** or ensure Claude processes incrementally
|
1. **Process one file at a time**
|
||||||
1. **Verify each test passes** before asking for the next
|
1. **Verify each test passes** before asking for the next
|
||||||
1. **Mark todos complete** as you progress
|
1. **Mark todos complete** as you progress
|
||||||
|
|
||||||
|
|||||||
71
.agents/skills/how-to-write-component/SKILL.md
Normal file
71
.agents/skills/how-to-write-component/SKILL.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
name: how-to-write-component
|
||||||
|
description: React/TypeScript component style guide. Use when writing, refactoring, or reviewing React components, especially around props typing, state boundaries, shared local state with Jotai atoms, API types, query/mutation contracts, navigation, memoization, wrappers, and empty-state handling.
|
||||||
|
---
|
||||||
|
|
||||||
|
# How To Write A Component
|
||||||
|
|
||||||
|
Use this as the decision guide for React/TypeScript component structure. Existing code is reference material, not automatic precedent; when it conflicts with these rules, adapt the approach instead of reproducing the violation.
|
||||||
|
|
||||||
|
## Core Defaults
|
||||||
|
|
||||||
|
- Search before adding UI, hooks, helpers, or styling patterns. Reuse existing base components, feature components, hooks, utilities, and design styles when they fit.
|
||||||
|
- Group code by feature workflow, route, or ownership area: components, hooks, local types, query helpers, atoms, constants, and small utilities should live near the code that changes with them.
|
||||||
|
- Promote code to shared only when multiple verticals need the same stable primitive. Otherwise keep it local and compose shared primitives inside the owning feature.
|
||||||
|
- Follow Dify's CSS-first Tailwind v4 contract from `packages/dify-ui/README.md` and `packages/dify-ui/AGENTS.md`. Prefer design-system tokens, utilities, and radius mappings over generic Tailwind guidance.
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Put local state, queries, mutations, handlers, and derived UI data in the lowest component that uses them. Extract a purpose-built owner component only when the logic has no natural home.
|
||||||
|
- Repeated TanStack query calls in sibling components are acceptable when each component independently consumes the data. Do not hoist a query only because it is duplicated; TanStack Query handles deduplication and cache sharing.
|
||||||
|
- Hoist state, queries, or callbacks to a parent only when the parent consumes the data, coordinates shared loading/error/empty UI, needs one consistent snapshot, or owns a workflow spanning children.
|
||||||
|
- Avoid prop drilling. One pass-through layer is acceptable; repeated forwarding means ownership should move down or into feature-scoped Jotai UI state. Keep server/cache state in query and API data flow.
|
||||||
|
- Keep callbacks in a parent only for workflow coordination such as form submission, shared selection, batch behavior, or navigation. Otherwise let the child or row own its action.
|
||||||
|
- Prefer uncontrolled DOM state and CSS variables before adding controlled props.
|
||||||
|
|
||||||
|
## Components, Props, And Types
|
||||||
|
|
||||||
|
- Type component signatures directly; do not use `FC` or `React.FC`.
|
||||||
|
- Prefer `function` for top-level components and module helpers. Use arrow functions for local callbacks, handlers, and lambda-style APIs.
|
||||||
|
- Prefer named exports. Use default exports only where the framework requires them, such as Next.js route files.
|
||||||
|
- Type simple one-off props inline. Use a named `Props` type only when reused, exported, complex, or clearer.
|
||||||
|
- Use API-generated or API-returned types at component boundaries. Keep small UI conversion helpers beside the component that needs them.
|
||||||
|
- Name values by their domain role and backend API contract, and keep that name stable across the call chain, especially IDs like `appInstanceId`. Normalize framework or route params at the boundary.
|
||||||
|
- Keep fallback and invariant checks at the lowest component that already handles that state; callers should pass raw values through instead of duplicating checks.
|
||||||
|
|
||||||
|
## Queries And Mutations
|
||||||
|
|
||||||
|
- Keep `web/contract/*` as the single source of truth for API shape; follow existing domain/router patterns and the `{ params, query?, body? }` input shape.
|
||||||
|
- Consume queries directly with `useQuery(consoleQuery.xxx.queryOptions(...))` or `useQuery(marketplaceQuery.xxx.queryOptions(...))`.
|
||||||
|
- Avoid pass-through hooks and thin `web/service/use-*` wrappers that only rename `queryOptions()` or `mutationOptions()`. Extract a small `queryOptions` helper only when repeated call-site options justify it.
|
||||||
|
- Keep feature hooks for real orchestration, workflow state, or shared domain behavior.
|
||||||
|
- For missing required query input, use `input: skipToken`; use `enabled` only for extra business gating after the input is valid.
|
||||||
|
- Consume mutations directly with `useMutation(consoleQuery.xxx.mutationOptions(...))` or `useMutation(marketplaceQuery.xxx.mutationOptions(...))`; use oRPC clients as `mutationFn` only for custom flows.
|
||||||
|
- Put shared cache behavior in `createTanstackQueryUtils(...experimental_defaults...)`; components may add UI feedback callbacks, but should not own shared invalidation rules.
|
||||||
|
- Do not use deprecated `useInvalid` or `useReset`.
|
||||||
|
- Prefer `mutate(...)`; use `mutateAsync(...)` only when Promise semantics are required, and wrap awaited calls in `try/catch`.
|
||||||
|
|
||||||
|
## Component Boundaries
|
||||||
|
|
||||||
|
- Use the first level below a page or tab to organize independent page sections when it adds real structure. This layer is layout/semantic first, not automatically the data owner.
|
||||||
|
- Split deeper components by the data and state each layer actually needs. Each component should access only necessary data, and ownership should stay at the lowest consumer.
|
||||||
|
- Keep cohesive forms, menu bodies, and one-off helpers local unless they need their own state, reuse, or semantic boundary.
|
||||||
|
- Separate hidden secondary surfaces from the trigger's main flow. For dialogs, dropdowns, popovers, and similar branches, extract a small local component that owns the trigger, open state, and hidden content when it would obscure the parent flow.
|
||||||
|
- Preserve composability by separating behavior ownership from layout ownership. A dropdown action may own its trigger, open state, and menu content; the caller owns placement such as slots, offsets, and alignment.
|
||||||
|
- Avoid unnecessary DOM hierarchy. Do not add wrapper elements unless they provide layout, semantics, accessibility, state ownership, or integration with a library API; prefer fragments or styling an existing element when possible.
|
||||||
|
- Avoid shallow wrappers and prop renaming unless the wrapper adds validation, orchestration, error handling, state ownership, or a real semantic boundary.
|
||||||
|
|
||||||
|
## You Might Not Need An Effect
|
||||||
|
|
||||||
|
- Use Effects only to synchronize with external systems such as browser APIs, non-React widgets, subscriptions, timers, analytics that must run because the component was shown, or imperative DOM integration.
|
||||||
|
- Do not use Effects to transform props or state for rendering. Calculate derived values during render, and use `useMemo` only when the calculation is actually expensive.
|
||||||
|
- Do not use Effects to handle user actions. Put action-specific logic in the event handler where the cause is known.
|
||||||
|
- Do not use Effects to copy one state value into another state value representing the same concept. Pick one source of truth and derive the rest during render.
|
||||||
|
- Do not reset or adjust state from props with an Effect. Prefer a `key` reset, storing a stable ID and deriving the selected object, or guarded same-component render-time adjustment when truly necessary.
|
||||||
|
- Prefer framework data APIs or TanStack Query for data fetching instead of writing request Effects in components.
|
||||||
|
- If an Effect still seems necessary, first name the external system it synchronizes with. If there is no external system, remove the Effect and restructure the state or event flow.
|
||||||
|
|
||||||
|
## Navigation And Performance
|
||||||
|
|
||||||
|
- Prefer `Link` for normal navigation. Use router APIs only for command-flow side effects such as mutation success, guarded redirects, or form submission.
|
||||||
|
- Avoid `memo`, `useMemo`, and `useCallback` unless there is a clear performance reason.
|
||||||
1
.claude/skills/e2e-cucumber-playwright
Symbolic link
1
.claude/skills/e2e-cucumber-playwright
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/e2e-cucumber-playwright
|
||||||
@ -1,5 +1,6 @@
|
|||||||
[run]
|
[run]
|
||||||
omit =
|
omit =
|
||||||
|
api/conftest.py
|
||||||
api/tests/*
|
api/tests/*
|
||||||
api/migrations/*
|
api/migrations/*
|
||||||
api/core/rag/datasource/vdb/*
|
api/core/rag/datasource/vdb/*
|
||||||
|
|||||||
@ -7,7 +7,7 @@ cd web && pnpm install
|
|||||||
pipx install uv
|
pipx install uv
|
||||||
|
|
||||||
echo "alias start-api=\"cd $WORKSPACE_ROOT/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug\"" >> ~/.bashrc
|
echo "alias start-api=\"cd $WORKSPACE_ROOT/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug\"" >> ~/.bashrc
|
||||||
echo "alias start-worker=\"cd $WORKSPACE_ROOT/api && uv run python -m celery -A app.celery worker -P threads -c 1 --loglevel INFO -Q dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_executor,retention,workflow_based_app_execution\"" >> ~/.bashrc
|
echo "alias start-worker=\"cd $WORKSPACE_ROOT/api && uv run python -m celery -A app.celery worker -P threads -c 1 --loglevel INFO -Q dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_publisher,trigger_refresh_executor,retention\"" >> ~/.bashrc
|
||||||
echo "alias start-web=\"cd $WORKSPACE_ROOT/web && pnpm dev:inspect\"" >> ~/.bashrc
|
echo "alias start-web=\"cd $WORKSPACE_ROOT/web && pnpm dev:inspect\"" >> ~/.bashrc
|
||||||
echo "alias start-web-prod=\"cd $WORKSPACE_ROOT/web && pnpm build && pnpm start\"" >> ~/.bashrc
|
echo "alias start-web-prod=\"cd $WORKSPACE_ROOT/web && pnpm build && pnpm start\"" >> ~/.bashrc
|
||||||
echo "alias start-containers=\"cd $WORKSPACE_ROOT/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d\"" >> ~/.bashrc
|
echo "alias start-containers=\"cd $WORKSPACE_ROOT/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d\"" >> ~/.bashrc
|
||||||
|
|||||||
64
.github/CODEOWNERS
vendored
64
.github/CODEOWNERS
vendored
@ -4,7 +4,10 @@
|
|||||||
# Owners can be @username, @org/team-name, or email addresses.
|
# Owners can be @username, @org/team-name, or email addresses.
|
||||||
# For more information, see: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
# For more information, see: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||||
|
|
||||||
* @crazywoola @laipz8200 @Yeuoly
|
* @crazywoola @laipz8200
|
||||||
|
|
||||||
|
# ESLint suppression file is maintained by autofix.ci pruning.
|
||||||
|
/eslint-suppressions.json
|
||||||
|
|
||||||
# CODEOWNERS file
|
# CODEOWNERS file
|
||||||
/.github/CODEOWNERS @laipz8200 @crazywoola
|
/.github/CODEOWNERS @laipz8200 @crazywoola
|
||||||
@ -36,7 +39,6 @@
|
|||||||
/api/core/workflow/graph/ @laipz8200 @QuantumGhost
|
/api/core/workflow/graph/ @laipz8200 @QuantumGhost
|
||||||
/api/core/workflow/graph_events/ @laipz8200 @QuantumGhost
|
/api/core/workflow/graph_events/ @laipz8200 @QuantumGhost
|
||||||
/api/core/workflow/node_events/ @laipz8200 @QuantumGhost
|
/api/core/workflow/node_events/ @laipz8200 @QuantumGhost
|
||||||
/api/dify_graph/model_runtime/ @laipz8200 @QuantumGhost
|
|
||||||
|
|
||||||
# Backend - Workflow - Nodes (Agent, Iteration, Loop, LLM)
|
# Backend - Workflow - Nodes (Agent, Iteration, Loop, LLM)
|
||||||
/api/core/workflow/nodes/agent/ @Nov1c444
|
/api/core/workflow/nodes/agent/ @Nov1c444
|
||||||
@ -83,39 +85,39 @@
|
|||||||
/api/tasks/deal_dataset_vector_index_task.py @JohnJyong
|
/api/tasks/deal_dataset_vector_index_task.py @JohnJyong
|
||||||
|
|
||||||
# Backend - Plugins
|
# Backend - Plugins
|
||||||
/api/core/plugin/ @Mairuis @Yeuoly @Stream29
|
/api/core/plugin/ @WH-2099
|
||||||
/api/services/plugin/ @Mairuis @Yeuoly @Stream29
|
/api/services/plugin/ @WH-2099
|
||||||
/api/controllers/console/workspace/plugin.py @Mairuis @Yeuoly @Stream29
|
/api/controllers/console/workspace/plugin.py @WH-2099
|
||||||
/api/controllers/inner_api/plugin/ @Mairuis @Yeuoly @Stream29
|
/api/controllers/inner_api/plugin/ @WH-2099
|
||||||
/api/tasks/process_tenant_plugin_autoupgrade_check_task.py @Mairuis @Yeuoly @Stream29
|
/api/tasks/process_tenant_plugin_autoupgrade_check_task.py @WH-2099
|
||||||
|
|
||||||
# Backend - Trigger/Schedule/Webhook
|
# Backend - Trigger/Schedule/Webhook
|
||||||
/api/controllers/trigger/ @Mairuis @Yeuoly
|
/api/controllers/trigger/ @CourTeous33
|
||||||
/api/controllers/console/app/workflow_trigger.py @Mairuis @Yeuoly
|
/api/controllers/console/app/workflow_trigger.py @CourTeous33
|
||||||
/api/controllers/console/workspace/trigger_providers.py @Mairuis @Yeuoly
|
/api/controllers/console/workspace/trigger_providers.py @CourTeous33
|
||||||
/api/core/trigger/ @Mairuis @Yeuoly
|
/api/core/trigger/ @CourTeous33
|
||||||
/api/core/app/layers/trigger_post_layer.py @Mairuis @Yeuoly
|
/api/core/app/layers/trigger_post_layer.py @CourTeous33
|
||||||
/api/services/trigger/ @Mairuis @Yeuoly
|
/api/services/trigger/ @CourTeous33
|
||||||
/api/models/trigger.py @Mairuis @Yeuoly
|
/api/models/trigger.py @CourTeous33
|
||||||
/api/fields/workflow_trigger_fields.py @Mairuis @Yeuoly
|
/api/fields/workflow_trigger_fields.py @CourTeous33
|
||||||
/api/repositories/workflow_trigger_log_repository.py @Mairuis @Yeuoly
|
/api/repositories/workflow_trigger_log_repository.py @CourTeous33
|
||||||
/api/repositories/sqlalchemy_workflow_trigger_log_repository.py @Mairuis @Yeuoly
|
/api/repositories/sqlalchemy_workflow_trigger_log_repository.py @CourTeous33
|
||||||
/api/libs/schedule_utils.py @Mairuis @Yeuoly
|
/api/libs/schedule_utils.py @CourTeous33
|
||||||
/api/services/workflow/scheduler.py @Mairuis @Yeuoly
|
/api/services/workflow/scheduler.py @CourTeous33
|
||||||
/api/schedule/trigger_provider_refresh_task.py @Mairuis @Yeuoly
|
/api/schedule/trigger_provider_refresh_task.py @CourTeous33
|
||||||
/api/schedule/workflow_schedule_task.py @Mairuis @Yeuoly
|
/api/schedule/workflow_schedule_task.py @CourTeous33
|
||||||
/api/tasks/trigger_processing_tasks.py @Mairuis @Yeuoly
|
/api/tasks/trigger_processing_tasks.py @CourTeous33
|
||||||
/api/tasks/trigger_subscription_refresh_tasks.py @Mairuis @Yeuoly
|
/api/tasks/trigger_subscription_refresh_tasks.py @CourTeous33
|
||||||
/api/tasks/workflow_schedule_tasks.py @Mairuis @Yeuoly
|
/api/tasks/workflow_schedule_tasks.py @CourTeous33
|
||||||
/api/tasks/workflow_cfs_scheduler/ @Mairuis @Yeuoly
|
/api/tasks/workflow_cfs_scheduler/ @CourTeous33
|
||||||
/api/events/event_handlers/sync_plugin_trigger_when_app_created.py @Mairuis @Yeuoly
|
/api/events/event_handlers/sync_plugin_trigger_when_app_created.py @CourTeous33
|
||||||
/api/events/event_handlers/update_app_triggers_when_app_published_workflow_updated.py @Mairuis @Yeuoly
|
/api/events/event_handlers/update_app_triggers_when_app_published_workflow_updated.py @CourTeous33
|
||||||
/api/events/event_handlers/sync_workflow_schedule_when_app_published.py @Mairuis @Yeuoly
|
/api/events/event_handlers/sync_workflow_schedule_when_app_published.py @CourTeous33
|
||||||
/api/events/event_handlers/sync_webhook_when_app_created.py @Mairuis @Yeuoly
|
/api/events/event_handlers/sync_webhook_when_app_created.py @CourTeous33
|
||||||
|
|
||||||
# Backend - Async Workflow
|
# Backend - Async Workflow
|
||||||
/api/services/async_workflow_service.py @Mairuis @Yeuoly
|
/api/services/async_workflow_service.py @Mairuis
|
||||||
/api/tasks/async_workflow_tasks.py @Mairuis @Yeuoly
|
/api/tasks/async_workflow_tasks.py @Mairuis
|
||||||
|
|
||||||
# Backend - Billing
|
# Backend - Billing
|
||||||
/api/services/billing_service.py @hj24 @zyssyz123
|
/api/services/billing_service.py @hj24 @zyssyz123
|
||||||
|
|||||||
10
.github/actions/setup-web/action.yml
vendored
10
.github/actions/setup-web/action.yml
vendored
@ -1,12 +1,16 @@
|
|||||||
name: Setup Web Environment
|
name: Setup Web Environment
|
||||||
|
description: Set up Node.js, Vite+, pnpm, and web dependencies
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Vite+
|
- name: Setup pnpm
|
||||||
uses: voidzero-dev/setup-vp@20553a7a7429c429a74894104a2835d7fed28a72 # v1.3.0
|
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
- name: Setup Vite+
|
||||||
|
uses: voidzero-dev/setup-vp@ca1c46663915d6c1042ae23bd39ab85718bfb0fa # v1.10.0
|
||||||
with:
|
with:
|
||||||
working-directory: web
|
|
||||||
node-version-file: .nvmrc
|
node-version-file: .nvmrc
|
||||||
cache: true
|
cache: true
|
||||||
run-install: true
|
run-install: true
|
||||||
|
|||||||
100
.github/dependabot.yml
vendored
100
.github/dependabot.yml
vendored
@ -1,106 +1,6 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "pip"
|
|
||||||
directory: "/api"
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
groups:
|
|
||||||
flask:
|
|
||||||
patterns:
|
|
||||||
- "flask"
|
|
||||||
- "flask-*"
|
|
||||||
- "werkzeug"
|
|
||||||
- "gunicorn"
|
|
||||||
google:
|
|
||||||
patterns:
|
|
||||||
- "google-*"
|
|
||||||
- "googleapis-*"
|
|
||||||
opentelemetry:
|
|
||||||
patterns:
|
|
||||||
- "opentelemetry-*"
|
|
||||||
pydantic:
|
|
||||||
patterns:
|
|
||||||
- "pydantic"
|
|
||||||
- "pydantic-*"
|
|
||||||
llm:
|
|
||||||
patterns:
|
|
||||||
- "langfuse"
|
|
||||||
- "langsmith"
|
|
||||||
- "litellm"
|
|
||||||
- "mlflow*"
|
|
||||||
- "opik"
|
|
||||||
- "weave*"
|
|
||||||
- "arize*"
|
|
||||||
- "tiktoken"
|
|
||||||
- "transformers"
|
|
||||||
database:
|
|
||||||
patterns:
|
|
||||||
- "sqlalchemy"
|
|
||||||
- "psycopg2*"
|
|
||||||
- "psycogreen"
|
|
||||||
- "redis*"
|
|
||||||
- "alembic*"
|
|
||||||
storage:
|
|
||||||
patterns:
|
|
||||||
- "boto3*"
|
|
||||||
- "botocore*"
|
|
||||||
- "azure-*"
|
|
||||||
- "bce-*"
|
|
||||||
- "cos-python-*"
|
|
||||||
- "esdk-obs-*"
|
|
||||||
- "google-cloud-storage"
|
|
||||||
- "opendal"
|
|
||||||
- "oss2"
|
|
||||||
- "supabase*"
|
|
||||||
- "tos*"
|
|
||||||
vdb:
|
|
||||||
patterns:
|
|
||||||
- "alibabacloud*"
|
|
||||||
- "chromadb"
|
|
||||||
- "clickhouse-*"
|
|
||||||
- "clickzetta-*"
|
|
||||||
- "couchbase"
|
|
||||||
- "elasticsearch"
|
|
||||||
- "opensearch-py"
|
|
||||||
- "oracledb"
|
|
||||||
- "pgvect*"
|
|
||||||
- "pymilvus"
|
|
||||||
- "pymochow"
|
|
||||||
- "pyobvector"
|
|
||||||
- "qdrant-client"
|
|
||||||
- "intersystems-*"
|
|
||||||
- "tablestore"
|
|
||||||
- "tcvectordb"
|
|
||||||
- "tidb-vector"
|
|
||||||
- "upstash-*"
|
|
||||||
- "volcengine-*"
|
|
||||||
- "weaviate-*"
|
|
||||||
- "xinference-*"
|
|
||||||
- "mo-vector"
|
|
||||||
- "mysql-connector-*"
|
|
||||||
dev:
|
|
||||||
patterns:
|
|
||||||
- "coverage"
|
|
||||||
- "dotenv-linter"
|
|
||||||
- "faker"
|
|
||||||
- "lxml-stubs"
|
|
||||||
- "basedpyright"
|
|
||||||
- "ruff"
|
|
||||||
- "pytest*"
|
|
||||||
- "types-*"
|
|
||||||
- "boto3-stubs"
|
|
||||||
- "hypothesis"
|
|
||||||
- "pandas-stubs"
|
|
||||||
- "scipy-stubs"
|
|
||||||
- "import-linter"
|
|
||||||
- "celery-types"
|
|
||||||
- "mypy*"
|
|
||||||
- "pyrefly"
|
|
||||||
python-packages:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
- package-ecosystem: "uv"
|
- package-ecosystem: "uv"
|
||||||
directory: "/api"
|
directory: "/api"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
|||||||
8
.github/labeler.yml
vendored
8
.github/labeler.yml
vendored
@ -1,3 +1,9 @@
|
|||||||
web:
|
web:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: 'web/**'
|
- any-glob-to-any-file:
|
||||||
|
- 'web/**'
|
||||||
|
- 'packages/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'pnpm-workspace.yaml'
|
||||||
|
- '.nvmrc'
|
||||||
|
|||||||
9
.github/pull_request_template.md
vendored
9
.github/pull_request_template.md
vendored
@ -7,6 +7,7 @@
|
|||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||||
|
<!-- If this PR was created by an automated agent, add `From <Tool Name>` as the final line of the description. Example: `From Codex`. -->
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@ -17,7 +18,7 @@
|
|||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] This change requires a documentation update, included: [Dify Document](https://github.com/langgenius/dify-docs)
|
- [ ] This change requires a documentation update, included: [Dify Document](https://github.com/langgenius/dify-docs)
|
||||||
- [x] I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
|
- [ ] I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
|
||||||
- [x] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
|
- [ ] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
|
||||||
- [x] I've updated the documentation accordingly.
|
- [ ] I've updated the documentation accordingly.
|
||||||
- [x] I ran `make lint` and `make type-check` (backend) and `cd web && npx lint-staged` (frontend) to appease the lint gods
|
- [ ] I ran `make lint && make type-check` (backend) and `cd web && pnpm exec vp staged` (frontend) to appease the lint gods
|
||||||
|
|||||||
73
.github/scripts/check-hotfix-cherry-picks.sh
vendored
Normal file
73
.github/scripts/check-hotfix-cherry-picks.sh
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE_SHA=${BASE_SHA:-}
|
||||||
|
HEAD_SHA=${HEAD_SHA:-}
|
||||||
|
MAIN_REF=${MAIN_REF:-origin/main}
|
||||||
|
REMEDIATION_HINT="Changes should be made from the main branch using git cherry-pick -x."
|
||||||
|
|
||||||
|
error() {
|
||||||
|
printf 'ERROR: %s\n' "$1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -z "$BASE_SHA" || -z "$HEAD_SHA" ]]; then
|
||||||
|
error "BASE_SHA and HEAD_SHA are required. $REMEDIATION_HINT"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git rev-parse --verify "$BASE_SHA^{commit}" > /dev/null 2>&1; then
|
||||||
|
error "Base commit '$BASE_SHA' is not available in the local git checkout."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git rev-parse --verify "$HEAD_SHA^{commit}" > /dev/null 2>&1; then
|
||||||
|
error "Head commit '$HEAD_SHA' is not available in the local git checkout."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git rev-parse --verify "$MAIN_REF^{commit}" > /dev/null 2>&1; then
|
||||||
|
error "Main ref '$MAIN_REF' is not available in the local git checkout. $REMEDIATION_HINT"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
checked=0
|
||||||
|
|
||||||
|
while IFS= read -r commit_sha; do
|
||||||
|
[[ -n "$commit_sha" ]] || continue
|
||||||
|
|
||||||
|
checked=$((checked + 1))
|
||||||
|
subject=$(git log -1 --format=%s "$commit_sha")
|
||||||
|
source_sha=$(
|
||||||
|
git log -1 --format=%B "$commit_sha" \
|
||||||
|
| sed -nE 's/^\(cherry picked from commit ([0-9a-fA-F]{7,64})\)$/\1/p' \
|
||||||
|
| tail -n 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ -z "$source_sha" ]]; then
|
||||||
|
error "Commit $commit_sha ($subject) is missing cherry-pick provenance. $REMEDIATION_HINT"
|
||||||
|
failed=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git cat-file -e "$source_sha^{commit}" 2> /dev/null; then
|
||||||
|
error "Commit $commit_sha ($subject) references source $source_sha, but that commit is not available locally. $REMEDIATION_HINT"
|
||||||
|
failed=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git merge-base --is-ancestor "$source_sha" "$MAIN_REF"; then
|
||||||
|
error "Commit $commit_sha ($subject) references source $source_sha, but that source is not reachable from main ($MAIN_REF). $REMEDIATION_HINT"
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done < <(git rev-list --reverse "$BASE_SHA..$HEAD_SHA")
|
||||||
|
|
||||||
|
if [[ "$failed" -ne 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$checked" -eq 0 ]]; then
|
||||||
|
echo "No PR commits to check."
|
||||||
|
else
|
||||||
|
echo "Verified $checked PR commit(s) include cherry-pick provenance from main."
|
||||||
|
fi
|
||||||
82
.github/scripts/generate-i18n-changes.mjs
vendored
Normal file
82
.github/scripts/generate-i18n-changes.mjs
vendored
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { execFileSync } from 'node:child_process'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const repoRoot = process.cwd()
|
||||||
|
const baseSha = process.env.BASE_SHA || ''
|
||||||
|
const headSha = process.env.HEAD_SHA || ''
|
||||||
|
const files = (process.env.CHANGED_FILES || '').split(/\s+/).filter(Boolean)
|
||||||
|
const outputPath = process.env.I18N_CHANGES_OUTPUT_PATH || '/tmp/i18n-changes.json'
|
||||||
|
|
||||||
|
const englishPath = fileStem => path.join(repoRoot, 'web', 'i18n', 'en-US', `${fileStem}.json`)
|
||||||
|
|
||||||
|
const readCurrentJson = (fileStem) => {
|
||||||
|
const filePath = englishPath(fileStem)
|
||||||
|
if (!fs.existsSync(filePath))
|
||||||
|
return null
|
||||||
|
|
||||||
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const readBaseJson = (fileStem) => {
|
||||||
|
if (!baseSha)
|
||||||
|
return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const relativePath = `web/i18n/en-US/${fileStem}.json`
|
||||||
|
const content = execFileSync('git', ['show', `${baseSha}:${relativePath}`], { encoding: 'utf8' })
|
||||||
|
return JSON.parse(content)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const compareJson = (beforeValue, afterValue) => JSON.stringify(beforeValue) === JSON.stringify(afterValue)
|
||||||
|
|
||||||
|
const changes = {}
|
||||||
|
|
||||||
|
for (const fileStem of files) {
|
||||||
|
const currentJson = readCurrentJson(fileStem)
|
||||||
|
const beforeJson = readBaseJson(fileStem) || {}
|
||||||
|
const afterJson = currentJson || {}
|
||||||
|
const added = {}
|
||||||
|
const updated = {}
|
||||||
|
const deleted = []
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(afterJson)) {
|
||||||
|
if (!(key in beforeJson)) {
|
||||||
|
added[key] = value
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!compareJson(beforeJson[key], value)) {
|
||||||
|
updated[key] = {
|
||||||
|
before: beforeJson[key],
|
||||||
|
after: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(beforeJson)) {
|
||||||
|
if (!(key in afterJson))
|
||||||
|
deleted.push(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
changes[fileStem] = {
|
||||||
|
fileDeleted: currentJson === null,
|
||||||
|
added,
|
||||||
|
updated,
|
||||||
|
deleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
outputPath,
|
||||||
|
JSON.stringify({
|
||||||
|
baseSha,
|
||||||
|
headSha,
|
||||||
|
files,
|
||||||
|
changes,
|
||||||
|
})
|
||||||
|
)
|
||||||
19
.github/workflows/anti-slop.yml
vendored
19
.github/workflows/anti-slop.yml
vendored
@ -1,19 +0,0 @@
|
|||||||
name: Anti-Slop PR Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, edited, synchronize]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
anti-slop:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: peakoss/anti-slop@85daca1880e9e1af197fc06ea03349daf08f4202 # v0.2.1
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
close-pr: false
|
|
||||||
failure-add-pr-labels: "needs-revision"
|
|
||||||
183
.github/workflows/api-tests.yml
vendored
183
.github/workflows/api-tests.yml
vendored
@ -14,18 +14,17 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
api-unit:
|
||||||
name: API Tests
|
name: API Unit Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
COVERAGE_FILE: coverage-unit
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- "3.11"
|
|
||||||
- "3.12"
|
- "3.12"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -36,7 +35,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@ -49,48 +48,154 @@ jobs:
|
|||||||
run: uv sync --project api --dev
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
- name: Run dify config tests
|
- name: Run dify config tests
|
||||||
run: uv run --project api dev/pytest/pytest_config_tests.py
|
run: uv run --project api pytest api/tests/unit_tests/configs/test_env_consistency.py
|
||||||
|
|
||||||
- name: Set up dotenvs
|
- name: Run Unit Tests
|
||||||
run: |
|
|
||||||
cp docker/.env.example docker/.env
|
|
||||||
cp docker/middleware.env.example docker/middleware.env
|
|
||||||
|
|
||||||
- name: Expose Service Ports
|
|
||||||
run: sh .github/workflows/expose_service_ports.sh
|
|
||||||
|
|
||||||
- name: Set up Sandbox
|
|
||||||
uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
|
|
||||||
with:
|
|
||||||
compose-file: |
|
|
||||||
docker/docker-compose.middleware.yaml
|
|
||||||
services: |
|
|
||||||
db_postgres
|
|
||||||
redis
|
|
||||||
sandbox
|
|
||||||
ssrf_proxy
|
|
||||||
|
|
||||||
- name: setup test config
|
|
||||||
run: |
|
|
||||||
cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env
|
|
||||||
|
|
||||||
- name: Run API Tests
|
|
||||||
env:
|
|
||||||
STORAGE_TYPE: opendal
|
|
||||||
OPENDAL_SCHEME: fs
|
|
||||||
OPENDAL_FS_ROOT: /tmp/dify-storage
|
|
||||||
run: |
|
run: |
|
||||||
uv run --project api pytest \
|
uv run --project api pytest \
|
||||||
|
-p no:benchmark \
|
||||||
|
--timeout "${PYTEST_TIMEOUT:-20}" \
|
||||||
|
-n auto \
|
||||||
|
api/tests/unit_tests \
|
||||||
|
api/providers/vdb/*/tests/unit_tests \
|
||||||
|
api/providers/trace/*/tests/unit_tests \
|
||||||
|
--ignore=api/tests/unit_tests/controllers
|
||||||
|
# Controller tests register Flask routes at import time, so keep them out of xdist.
|
||||||
|
uv run --project api pytest \
|
||||||
|
--timeout "${PYTEST_TIMEOUT:-20}" \
|
||||||
|
--cov-append \
|
||||||
|
api/tests/unit_tests/controllers
|
||||||
|
|
||||||
|
- name: Upload unit coverage data
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: api-coverage-unit
|
||||||
|
path: coverage-unit
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
api-integration:
|
||||||
|
name: API Integration Tests
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
env:
|
||||||
|
COVERAGE_FILE: coverage-integration
|
||||||
|
STORAGE_TYPE: opendal
|
||||||
|
OPENDAL_SCHEME: fs
|
||||||
|
OPENDAL_FS_ROOT: /tmp/dify-storage
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version:
|
||||||
|
- "3.12"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup UV and Python
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache-dependency-glob: api/uv.lock
|
||||||
|
|
||||||
|
- name: Check UV lockfile
|
||||||
|
run: uv lock --project api --check
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
|
- name: Run Integration Tests
|
||||||
|
run: |
|
||||||
|
uv run --project api pytest \
|
||||||
|
-p no:benchmark \
|
||||||
|
--start-middleware \
|
||||||
-n auto \
|
-n auto \
|
||||||
--timeout "${PYTEST_TIMEOUT:-180}" \
|
--timeout "${PYTEST_TIMEOUT:-180}" \
|
||||||
api/tests/integration_tests/workflow \
|
api/tests/integration_tests/workflow \
|
||||||
api/tests/integration_tests/tools \
|
api/tests/integration_tests/tools \
|
||||||
api/tests/test_containers_integration_tests \
|
api/tests/test_containers_integration_tests
|
||||||
api/tests/unit_tests
|
|
||||||
|
- name: Upload integration coverage data
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: api-coverage-integration
|
||||||
|
path: coverage-integration
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
api-coverage:
|
||||||
|
name: API Coverage
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- api-unit
|
||||||
|
- api-integration
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
COVERAGE_FILE: .coverage
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup UV and Python
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
python-version: "3.12"
|
||||||
|
cache-dependency-glob: api/uv.lock
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
|
- name: Download coverage data
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
path: coverage-data
|
||||||
|
pattern: api-coverage-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Combine coverage
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "### API Coverage" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "Merged backend coverage report generated for Codecov project status." >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
unit_coverage="$(find coverage-data -type f -name coverage-unit -print -quit)"
|
||||||
|
integration_coverage="$(find coverage-data -type f -name coverage-integration -print -quit)"
|
||||||
|
: "${unit_coverage:?coverage-unit artifact not found}"
|
||||||
|
: "${integration_coverage:?coverage-integration artifact not found}"
|
||||||
|
|
||||||
|
report_file="$(mktemp)"
|
||||||
|
uv run --project api coverage combine "$unit_coverage" "$integration_coverage"
|
||||||
|
uv run --project api coverage report --show-missing | tee "$report_file"
|
||||||
|
echo "Summary: \`$(tail -n 1 "$report_file")\`" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
{
|
||||||
|
echo ""
|
||||||
|
echo "<details><summary>Coverage report</summary>"
|
||||||
|
echo ""
|
||||||
|
echo '```'
|
||||||
|
cat "$report_file"
|
||||||
|
echo '```'
|
||||||
|
echo "</details>"
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
uv run --project api coverage xml -o coverage.xml
|
||||||
|
|
||||||
- name: Report coverage
|
- name: Report coverage
|
||||||
if: ${{ env.CODECOV_TOKEN != '' && matrix.python-version == '3.12' }}
|
if: ${{ env.CODECOV_TOKEN != '' }}
|
||||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||||
with:
|
with:
|
||||||
files: ./coverage.xml
|
files: ./coverage.xml
|
||||||
disable_search: true
|
disable_search: true
|
||||||
|
|||||||
60
.github/workflows/autofix.yml
vendored
60
.github/workflows/autofix.yml
vendored
@ -2,6 +2,9 @@ name: autofix.ci
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
merge_group:
|
||||||
|
branches: ["main"]
|
||||||
|
types: [checks_requested]
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
permissions:
|
permissions:
|
||||||
@ -10,13 +13,19 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
autofix:
|
autofix:
|
||||||
if: github.repository == 'langgenius/dify'
|
if: github.repository == 'langgenius/dify'
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- name: Complete merge group check
|
||||||
|
if: github.event_name == 'merge_group'
|
||||||
|
run: echo "autofix.ci updates pull request branches, not merge group refs."
|
||||||
|
|
||||||
|
- if: github.event_name != 'merge_group'
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Check Docker Compose inputs
|
- name: Check Docker Compose inputs
|
||||||
|
if: github.event_name != 'merge_group'
|
||||||
id: docker-compose-changes
|
id: docker-compose-changes
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
docker/generate_docker_compose
|
docker/generate_docker_compose
|
||||||
@ -24,30 +33,39 @@ jobs:
|
|||||||
docker/docker-compose-template.yaml
|
docker/docker-compose-template.yaml
|
||||||
docker/docker-compose.yaml
|
docker/docker-compose.yaml
|
||||||
- name: Check web inputs
|
- name: Check web inputs
|
||||||
|
if: github.event_name != 'merge_group'
|
||||||
id: web-changes
|
id: web-changes
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
web/**
|
web/**
|
||||||
|
packages/**
|
||||||
|
package.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
pnpm-workspace.yaml
|
||||||
|
.nvmrc
|
||||||
- name: Check api inputs
|
- name: Check api inputs
|
||||||
|
if: github.event_name != 'merge_group'
|
||||||
id: api-changes
|
id: api-changes
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
api/**
|
api/**
|
||||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
- if: github.event_name != 'merge_group'
|
||||||
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
- if: github.event_name != 'merge_group'
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
|
||||||
- name: Generate Docker Compose
|
- name: Generate Docker Compose
|
||||||
if: steps.docker-compose-changes.outputs.any_changed == 'true'
|
if: github.event_name != 'merge_group' && steps.docker-compose-changes.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
cd docker
|
cd docker
|
||||||
./generate_docker_compose
|
./generate_docker_compose
|
||||||
|
|
||||||
- if: steps.api-changes.outputs.any_changed == 'true'
|
- if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
cd api
|
cd api
|
||||||
uv sync --dev
|
uv sync --dev
|
||||||
@ -59,13 +77,13 @@ jobs:
|
|||||||
uv run ruff format ..
|
uv run ruff format ..
|
||||||
|
|
||||||
- name: count migration progress
|
- name: count migration progress
|
||||||
if: steps.api-changes.outputs.any_changed == 'true'
|
if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
cd api
|
cd api
|
||||||
./cnt_base.sh
|
./cnt_base.sh
|
||||||
|
|
||||||
- name: ast-grep
|
- name: ast-grep
|
||||||
if: steps.api-changes.outputs.any_changed == 'true'
|
if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
# ast-grep exits 1 if no matches are found; allow idempotent runs.
|
# ast-grep exits 1 if no matches are found; allow idempotent runs.
|
||||||
uvx --from ast-grep-cli ast-grep --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all || true
|
uvx --from ast-grep-cli ast-grep --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all || true
|
||||||
@ -95,13 +113,23 @@ jobs:
|
|||||||
find . -name "*.py.bak" -type f -delete
|
find . -name "*.py.bak" -type f -delete
|
||||||
|
|
||||||
- name: Setup web environment
|
- name: Setup web environment
|
||||||
if: steps.web-changes.outputs.any_changed == 'true'
|
if: github.event_name != 'merge_group'
|
||||||
uses: ./.github/actions/setup-web
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
- name: ESLint autofix
|
- name: Generate API docs
|
||||||
if: steps.web-changes.outputs.any_changed == 'true'
|
if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
cd api
|
||||||
|
uv run dev/generate_swagger_markdown_docs.py --swagger-dir ../packages/contracts/openapi --markdown-dir openapi/markdown --keep-swagger-json
|
||||||
|
|
||||||
|
- name: Generate frontend contracts
|
||||||
|
if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
|
||||||
|
run: pnpm --dir packages/contracts gen-api-contract-from-openapi
|
||||||
|
|
||||||
|
- name: ESLint autofix
|
||||||
|
if: github.event_name != 'merge_group' && steps.web-changes.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
cd web
|
|
||||||
vp exec eslint --concurrency=2 --prune-suppressions --quiet || true
|
vp exec eslint --concurrency=2 --prune-suppressions --quiet || true
|
||||||
|
|
||||||
- uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8 # v1.3.3
|
- if: github.event_name != 'merge_group'
|
||||||
|
uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a # v1.3.4
|
||||||
|
|||||||
78
.github/workflows/build-push.yml
vendored
78
.github/workflows/build-push.yml
vendored
@ -24,27 +24,42 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ${{ matrix.platform == 'linux/arm64' && 'arm64_runner' || 'ubuntu-latest' }}
|
runs-on: ${{ matrix.runs_on }}
|
||||||
if: github.repository == 'langgenius/dify'
|
if: github.repository == 'langgenius/dify'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- service_name: "build-api-amd64"
|
- service_name: "build-api-amd64"
|
||||||
image_name_env: "DIFY_API_IMAGE_NAME"
|
image_name_env: "DIFY_API_IMAGE_NAME"
|
||||||
context: "api"
|
artifact_context: "api"
|
||||||
|
build_context: "{{defaultContext}}"
|
||||||
|
file: "api/Dockerfile"
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
|
runs_on: depot-ubuntu-24.04-4
|
||||||
- service_name: "build-api-arm64"
|
- service_name: "build-api-arm64"
|
||||||
image_name_env: "DIFY_API_IMAGE_NAME"
|
image_name_env: "DIFY_API_IMAGE_NAME"
|
||||||
context: "api"
|
artifact_context: "api"
|
||||||
|
build_context: "{{defaultContext}}"
|
||||||
|
file: "api/Dockerfile"
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
|
runs_on: depot-ubuntu-24.04-4
|
||||||
- service_name: "build-web-amd64"
|
- service_name: "build-web-amd64"
|
||||||
image_name_env: "DIFY_WEB_IMAGE_NAME"
|
image_name_env: "DIFY_WEB_IMAGE_NAME"
|
||||||
context: "web"
|
artifact_context: "web"
|
||||||
|
build_context: "{{defaultContext}}"
|
||||||
|
file: "web/Dockerfile"
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
|
runs_on: depot-ubuntu-24.04-4
|
||||||
- service_name: "build-web-arm64"
|
- service_name: "build-web-arm64"
|
||||||
image_name_env: "DIFY_WEB_IMAGE_NAME"
|
image_name_env: "DIFY_WEB_IMAGE_NAME"
|
||||||
context: "web"
|
artifact_context: "web"
|
||||||
|
build_context: "{{defaultContext}}"
|
||||||
|
file: "web/Dockerfile"
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
|
runs_on: depot-ubuntu-24.04-4
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
@ -53,16 +68,13 @@ jobs:
|
|||||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKERHUB_USER }}
|
username: ${{ env.DOCKERHUB_USER }}
|
||||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up Depot CLI
|
||||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
|
||||||
|
|
||||||
- name: Extract metadata for Docker
|
- name: Extract metadata for Docker
|
||||||
id: meta
|
id: meta
|
||||||
@ -72,17 +84,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0
|
||||||
with:
|
with:
|
||||||
context: "{{defaultContext}}:${{ matrix.context }}"
|
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||||
|
context: ${{ matrix.build_context }}
|
||||||
|
file: ${{ matrix.file }}
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: ${{ matrix.platform }}
|
||||||
build-args: |
|
build-args: COMMIT_SHA=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||||
COMMIT_SHA=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
|
||||||
ENABLE_PROD_SOURCEMAP=${{ matrix.context == 'web' && github.ref_name == 'deploy/dev' }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
outputs: type=image,name=${{ env[matrix.image_name_env] }},push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,name=${{ env[matrix.image_name_env] }},push-by-digest=true,name-canonical=true,push=true
|
||||||
cache-from: type=gha,scope=${{ matrix.service_name }}
|
|
||||||
cache-to: type=gha,mode=max,scope=${{ matrix.service_name }}
|
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
env:
|
env:
|
||||||
@ -93,16 +103,40 @@ jobs:
|
|||||||
touch "/tmp/digests/${sanitized_digest}"
|
touch "/tmp/digests/${sanitized_digest}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-${{ matrix.context }}-${{ env.PLATFORM_PAIR }}
|
name: digests-${{ matrix.artifact_context }}-${{ env.PLATFORM_PAIR }}
|
||||||
path: /tmp/digests/*
|
path: /tmp/digests/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
|
fork-build-validate:
|
||||||
|
if: github.repository != 'langgenius/dify'
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- service_name: "validate-api-amd64"
|
||||||
|
build_context: "{{defaultContext}}"
|
||||||
|
file: "api/Dockerfile"
|
||||||
|
- service_name: "validate-web-amd64"
|
||||||
|
build_context: "{{defaultContext}}"
|
||||||
|
file: "web/Dockerfile"
|
||||||
|
steps:
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
|
- name: Validate Docker image
|
||||||
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
|
with:
|
||||||
|
push: false
|
||||||
|
context: ${{ matrix.build_context }}
|
||||||
|
file: ${{ matrix.file }}
|
||||||
|
platforms: linux/amd64
|
||||||
|
|
||||||
create-manifest:
|
create-manifest:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
if: github.repository == 'langgenius/dify'
|
if: github.repository == 'langgenius/dify'
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@ -122,7 +156,7 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKERHUB_USER }}
|
username: ${{ env.DOCKERHUB_USER }}
|
||||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||||
|
|||||||
38
.github/workflows/db-migration-test.yml
vendored
38
.github/workflows/db-migration-test.yml
vendored
@ -9,7 +9,7 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
db-migration-test-postgres:
|
db-migration-test-postgres:
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@ -19,7 +19,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
@ -37,10 +37,10 @@ jobs:
|
|||||||
- name: Prepare middleware env
|
- name: Prepare middleware env
|
||||||
run: |
|
run: |
|
||||||
cd docker
|
cd docker
|
||||||
cp middleware.env.example middleware.env
|
cp envs/middleware.env.example middleware.env
|
||||||
|
|
||||||
- name: Set up Middlewares
|
- name: Set up Middlewares
|
||||||
uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
|
uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
|
||||||
with:
|
with:
|
||||||
compose-file: |
|
compose-file: |
|
||||||
docker/docker-compose.middleware.yaml
|
docker/docker-compose.middleware.yaml
|
||||||
@ -59,7 +59,7 @@ jobs:
|
|||||||
run: uv run --directory api flask upgrade-db
|
run: uv run --directory api flask upgrade-db
|
||||||
|
|
||||||
db-migration-test-mysql:
|
db-migration-test-mysql:
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@ -69,7 +69,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
@ -87,14 +87,14 @@ jobs:
|
|||||||
- name: Prepare middleware env for MySQL
|
- name: Prepare middleware env for MySQL
|
||||||
run: |
|
run: |
|
||||||
cd docker
|
cd docker
|
||||||
cp middleware.env.example middleware.env
|
cp envs/middleware.env.example middleware.env
|
||||||
sed -i 's/DB_TYPE=postgresql/DB_TYPE=mysql/' middleware.env
|
sed -i 's/DB_TYPE=postgresql/DB_TYPE=mysql/' middleware.env
|
||||||
sed -i 's/DB_HOST=db_postgres/DB_HOST=db_mysql/' middleware.env
|
sed -i 's/DB_HOST=db_postgres/DB_HOST=db_mysql/' middleware.env
|
||||||
sed -i 's/DB_PORT=5432/DB_PORT=3306/' middleware.env
|
sed -i 's/DB_PORT=5432/DB_PORT=3306/' middleware.env
|
||||||
sed -i 's/DB_USERNAME=postgres/DB_USERNAME=mysql/' middleware.env
|
sed -i 's/DB_USERNAME=postgres/DB_USERNAME=mysql/' middleware.env
|
||||||
|
|
||||||
- name: Set up Middlewares
|
- name: Set up Middlewares
|
||||||
uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
|
uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
|
||||||
with:
|
with:
|
||||||
compose-file: |
|
compose-file: |
|
||||||
docker/docker-compose.middleware.yaml
|
docker/docker-compose.middleware.yaml
|
||||||
@ -110,6 +110,28 @@ jobs:
|
|||||||
sed -i 's/DB_PORT=5432/DB_PORT=3306/' .env
|
sed -i 's/DB_PORT=5432/DB_PORT=3306/' .env
|
||||||
sed -i 's/DB_USERNAME=postgres/DB_USERNAME=root/' .env
|
sed -i 's/DB_USERNAME=postgres/DB_USERNAME=root/' .env
|
||||||
|
|
||||||
|
# hoverkraft-tech/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
|
||||||
|
|||||||
2
.github/workflows/deploy-agent-dev.yml
vendored
2
.github/workflows/deploy-agent-dev.yml
vendored
@ -13,7 +13,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
github.event.workflow_run.head_branch == 'deploy/agent-dev'
|
github.event.workflow_run.head_branch == 'deploy/agent-dev'
|
||||||
|
|||||||
2
.github/workflows/deploy-dev.yml
vendored
2
.github/workflows/deploy-dev.yml
vendored
@ -10,7 +10,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
github.event.workflow_run.head_branch == 'deploy/dev'
|
github.event.workflow_run.head_branch == 'deploy/dev'
|
||||||
|
|||||||
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: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
github.event.workflow_run.head_branch == 'deploy/enterprise'
|
github.event.workflow_run.head_branch == 'deploy/enterprise'
|
||||||
|
|||||||
2
.github/workflows/deploy-hitl.yml
vendored
2
.github/workflows/deploy-hitl.yml
vendored
@ -10,7 +10,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
github.event.workflow_run.head_branch == 'build/feat/hitl'
|
github.event.workflow_run.head_branch == 'build/feat/hitl'
|
||||||
|
|||||||
66
.github/workflows/docker-build.yml
vendored
66
.github/workflows/docker-build.yml
vendored
@ -6,6 +6,12 @@ on:
|
|||||||
- "main"
|
- "main"
|
||||||
paths:
|
paths:
|
||||||
- api/Dockerfile
|
- api/Dockerfile
|
||||||
|
- api/Dockerfile.dockerignore
|
||||||
|
- api/pyproject.toml
|
||||||
|
- api/uv.lock
|
||||||
|
- dify-agent/pyproject.toml
|
||||||
|
- dify-agent/README.md
|
||||||
|
- dify-agent/src/**
|
||||||
- web/Dockerfile
|
- web/Dockerfile
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@ -14,35 +20,69 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-docker:
|
build-docker:
|
||||||
runs-on: ubuntu-latest
|
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
runs-on: ${{ matrix.runs_on }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- service_name: "api-amd64"
|
- service_name: "api-amd64"
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
context: "api"
|
runs_on: depot-ubuntu-24.04-4
|
||||||
|
context: "{{defaultContext}}"
|
||||||
|
file: "api/Dockerfile"
|
||||||
- service_name: "api-arm64"
|
- service_name: "api-arm64"
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
context: "api"
|
runs_on: depot-ubuntu-24.04-4
|
||||||
|
context: "{{defaultContext}}"
|
||||||
|
file: "api/Dockerfile"
|
||||||
- service_name: "web-amd64"
|
- service_name: "web-amd64"
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
context: "web"
|
runs_on: depot-ubuntu-24.04-4
|
||||||
|
context: "{{defaultContext}}"
|
||||||
|
file: "web/Dockerfile"
|
||||||
- service_name: "web-arm64"
|
- service_name: "web-arm64"
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
context: "web"
|
runs_on: depot-ubuntu-24.04-4
|
||||||
|
context: "{{defaultContext}}"
|
||||||
|
file: "web/Dockerfile"
|
||||||
steps:
|
steps:
|
||||||
- name: Set up QEMU
|
- name: Set up Depot CLI
|
||||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
|
||||||
|
|
||||||
|
- name: Build Docker Image
|
||||||
|
uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0
|
||||||
|
with:
|
||||||
|
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||||
|
push: false
|
||||||
|
context: ${{ matrix.context }}
|
||||||
|
file: ${{ matrix.file }}
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
build-docker-fork:
|
||||||
|
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- service_name: "api-amd64"
|
||||||
|
context: "{{defaultContext}}"
|
||||||
|
file: "api/Dockerfile"
|
||||||
|
- service_name: "web-amd64"
|
||||||
|
context: "{{defaultContext}}"
|
||||||
|
file: "web/Dockerfile"
|
||||||
|
steps:
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
push: false
|
push: false
|
||||||
context: "{{defaultContext}}:${{ matrix.context }}"
|
context: ${{ matrix.context }}
|
||||||
file: "${{ matrix.file }}"
|
file: ${{ matrix.file }}
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: linux/amd64
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|||||||
17
.github/workflows/expose_service_ports.sh
vendored
17
.github/workflows/expose_service_ports.sh
vendored
@ -1,17 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
yq eval '.services.weaviate.ports += ["8080:8080"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.weaviate.ports += ["50051:50051"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.qdrant.ports += ["6333:6333"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.chroma.ports += ["8000:8000"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services["milvus-standalone"].ports += ["19530:19530"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.pgvector.ports += ["5433:5432"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services["pgvecto-rs"].ports += ["5431:5432"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services["elasticsearch"].ports += ["9200:9200"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.couchbase-server.ports += ["8091-8096:8091-8096"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.couchbase-server.ports += ["11210:11210"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.tidb.ports += ["4000:4000"]' -i docker/tidb/docker-compose.yaml
|
|
||||||
yq eval '.services.oceanbase.ports += ["2881:2881"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.opengauss.ports += ["6600:6600"]' -i docker/docker-compose.yaml
|
|
||||||
|
|
||||||
echo "Ports exposed for sandbox, weaviate (HTTP 8080, gRPC 50051), tidb, qdrant, chroma, milvus, pgvector, pgvecto-rs, elasticsearch, couchbase, opengauss"
|
|
||||||
49
.github/workflows/hotfix-cherry-pick.yml
vendored
Normal file
49
.github/workflows/hotfix-cherry-pick.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
name: Hotfix Cherry-Pick Provenance
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'hotfix/**'
|
||||||
|
- 'lts/**'
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- edited
|
||||||
|
- reopened
|
||||||
|
- ready_for_review
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: hotfix-cherry-pick-${{ github.event.pull_request.number || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-cherry-pick-provenance:
|
||||||
|
name: Require cherry-pick provenance
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fetch PR base, PR head, and main
|
||||||
|
env:
|
||||||
|
BASE_REF: ${{ github.base_ref }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
run: |
|
||||||
|
git fetch --no-tags --prune origin \
|
||||||
|
"+refs/heads/main:refs/remotes/origin/main" \
|
||||||
|
"+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" \
|
||||||
|
"+refs/pull/${PR_NUMBER}/head:refs/remotes/pull/${PR_NUMBER}/head"
|
||||||
|
|
||||||
|
- name: Load checker from main
|
||||||
|
run: git show origin/main:.github/scripts/check-hotfix-cherry-picks.sh > "$RUNNER_TEMP/check-hotfix-cherry-picks.sh"
|
||||||
|
|
||||||
|
- name: Check PR commits
|
||||||
|
env:
|
||||||
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
MAIN_REF: origin/main
|
||||||
|
run: bash "$RUNNER_TEMP/check-hotfix-cherry-picks.sh"
|
||||||
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: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
||||||
with:
|
with:
|
||||||
sync-labels: true
|
sync-labels: true
|
||||||
|
|||||||
382
.github/workflows/main-ci.yml
vendored
382
.github/workflows/main-ci.yml
vendored
@ -3,10 +3,14 @@ name: Main CI Pipeline
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
merge_group:
|
||||||
|
branches: ["main"]
|
||||||
|
types: [checks_requested]
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
actions: write
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
checks: write
|
checks: write
|
||||||
@ -17,12 +21,28 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
pre_job:
|
||||||
|
name: Skip Duplicate Checks
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
outputs:
|
||||||
|
should_skip: ${{ steps.skip_check.outputs.should_skip || 'false' }}
|
||||||
|
steps:
|
||||||
|
- id: skip_check
|
||||||
|
continue-on-error: true
|
||||||
|
uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1
|
||||||
|
with:
|
||||||
|
cancel_others: 'true'
|
||||||
|
concurrent_skipping: same_content_newer
|
||||||
|
|
||||||
# Check which paths were changed to determine which tests to run
|
# Check which paths were changed to determine which tests to run
|
||||||
check-changes:
|
check-changes:
|
||||||
name: Check Changed Files
|
name: Check Changed Files
|
||||||
runs-on: ubuntu-latest
|
needs: pre_job
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
outputs:
|
outputs:
|
||||||
api-changed: ${{ steps.changes.outputs.api }}
|
api-changed: ${{ steps.changes.outputs.api }}
|
||||||
|
e2e-changed: ${{ steps.changes.outputs.e2e }}
|
||||||
web-changed: ${{ steps.changes.outputs.web }}
|
web-changed: ${{ steps.changes.outputs.web }}
|
||||||
vdb-changed: ${{ steps.changes.outputs.vdb }}
|
vdb-changed: ${{ steps.changes.outputs.vdb }}
|
||||||
migration-changed: ${{ steps.changes.outputs.migration }}
|
migration-changed: ${{ steps.changes.outputs.migration }}
|
||||||
@ -34,49 +54,375 @@ jobs:
|
|||||||
filters: |
|
filters: |
|
||||||
api:
|
api:
|
||||||
- 'api/**'
|
- 'api/**'
|
||||||
- 'docker/**'
|
|
||||||
- '.github/workflows/api-tests.yml'
|
- '.github/workflows/api-tests.yml'
|
||||||
|
- 'docker/.env.example'
|
||||||
|
- 'docker/envs/middleware.env.example'
|
||||||
|
- 'docker/docker-compose.middleware.yaml'
|
||||||
|
- 'docker/docker-compose-template.yaml'
|
||||||
|
- 'docker/generate_docker_compose'
|
||||||
|
- 'docker/ssrf_proxy/**'
|
||||||
|
- 'docker/volumes/sandbox/conf/**'
|
||||||
web:
|
web:
|
||||||
- 'web/**'
|
- 'web/**'
|
||||||
|
- 'packages/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'pnpm-workspace.yaml'
|
||||||
|
- '.nvmrc'
|
||||||
- '.github/workflows/web-tests.yml'
|
- '.github/workflows/web-tests.yml'
|
||||||
- '.github/actions/setup-web/**'
|
- '.github/actions/setup-web/**'
|
||||||
|
e2e:
|
||||||
|
- 'api/**'
|
||||||
|
- 'api/pyproject.toml'
|
||||||
|
- 'api/uv.lock'
|
||||||
|
- 'e2e/**'
|
||||||
|
- 'web/**'
|
||||||
|
- 'packages/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'pnpm-workspace.yaml'
|
||||||
|
- '.nvmrc'
|
||||||
|
- 'docker/docker-compose.middleware.yaml'
|
||||||
|
- 'docker/envs/middleware.env.example'
|
||||||
|
- '.github/workflows/web-e2e.yml'
|
||||||
|
- '.github/actions/setup-web/**'
|
||||||
vdb:
|
vdb:
|
||||||
- 'api/core/rag/datasource/**'
|
- 'api/core/rag/datasource/**'
|
||||||
- 'docker/**'
|
- 'api/tests/integration_tests/vdb/**'
|
||||||
|
- 'api/conftest.py'
|
||||||
|
- 'api/tests/pytest_dify.py'
|
||||||
|
- 'api/providers/vdb/*/tests/**'
|
||||||
- '.github/workflows/vdb-tests.yml'
|
- '.github/workflows/vdb-tests.yml'
|
||||||
|
- 'docker/.env.example'
|
||||||
|
- 'docker/envs/middleware.env.example'
|
||||||
|
- 'docker/docker-compose.pytest.ports.yaml'
|
||||||
|
- 'docker/docker-compose.yaml'
|
||||||
|
- 'docker/docker-compose-template.yaml'
|
||||||
|
- 'docker/generate_docker_compose'
|
||||||
|
- 'docker/certbot/**'
|
||||||
|
- 'docker/couchbase-server/**'
|
||||||
|
- 'docker/elasticsearch/**'
|
||||||
|
- 'docker/iris/**'
|
||||||
|
- 'docker/nginx/**'
|
||||||
|
- 'docker/pgvector/**'
|
||||||
|
- 'docker/ssrf_proxy/**'
|
||||||
|
- 'docker/startupscripts/**'
|
||||||
|
- 'docker/tidb/**'
|
||||||
|
- 'docker/volumes/**'
|
||||||
- 'api/uv.lock'
|
- 'api/uv.lock'
|
||||||
- 'api/pyproject.toml'
|
- 'api/pyproject.toml'
|
||||||
migration:
|
migration:
|
||||||
- 'api/migrations/**'
|
- 'api/migrations/**'
|
||||||
|
- 'api/.env.example'
|
||||||
- '.github/workflows/db-migration-test.yml'
|
- '.github/workflows/db-migration-test.yml'
|
||||||
|
- 'docker/.env.example'
|
||||||
|
- 'docker/envs/middleware.env.example'
|
||||||
|
- 'docker/docker-compose.middleware.yaml'
|
||||||
|
- 'docker/docker-compose-template.yaml'
|
||||||
|
- 'docker/generate_docker_compose'
|
||||||
|
- 'docker/ssrf_proxy/**'
|
||||||
|
- 'docker/volumes/sandbox/conf/**'
|
||||||
|
|
||||||
# Run tests in parallel
|
# Run tests in parallel while always emitting stable required checks.
|
||||||
api-tests:
|
api-tests-run:
|
||||||
name: API Tests
|
name: Run API Tests
|
||||||
needs: check-changes
|
needs:
|
||||||
if: needs.check-changes.outputs.api-changed == 'true'
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.api-changed == 'true'
|
||||||
uses: ./.github/workflows/api-tests.yml
|
uses: ./.github/workflows/api-tests.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
web-tests:
|
api-tests-skip:
|
||||||
name: Web Tests
|
name: Skip API Tests
|
||||||
needs: check-changes
|
needs:
|
||||||
if: needs.check-changes.outputs.web-changed == 'true'
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.api-changed != 'true'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Report skipped API tests
|
||||||
|
run: echo "No API-related changes detected; skipping API tests."
|
||||||
|
|
||||||
|
api-tests:
|
||||||
|
name: API Tests
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
- api-tests-run
|
||||||
|
- api-tests-skip
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Finalize API Tests status
|
||||||
|
env:
|
||||||
|
SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
|
||||||
|
TESTS_CHANGED: ${{ needs.check-changes.outputs.api-changed }}
|
||||||
|
RUN_RESULT: ${{ needs.api-tests-run.result }}
|
||||||
|
SKIP_RESULT: ${{ needs.api-tests-skip.result }}
|
||||||
|
run: |
|
||||||
|
if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
|
||||||
|
echo "API tests were skipped because this workflow run duplicated a successful or newer run."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$TESTS_CHANGED" == 'true' ]]; then
|
||||||
|
if [[ "$RUN_RESULT" == 'success' ]]; then
|
||||||
|
echo "API tests ran successfully."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "API tests were required but finished with result: $RUN_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SKIP_RESULT" == 'success' ]]; then
|
||||||
|
echo "API tests were skipped because no API-related files changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "API tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
web-tests-run:
|
||||||
|
name: Run Web Tests
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.web-changed == 'true'
|
||||||
uses: ./.github/workflows/web-tests.yml
|
uses: ./.github/workflows/web-tests.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
web-tests-skip:
|
||||||
|
name: Skip Web Tests
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.web-changed != 'true'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Report skipped web tests
|
||||||
|
run: echo "No web-related changes detected; skipping web tests."
|
||||||
|
|
||||||
|
web-tests:
|
||||||
|
name: Web Tests
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
- web-tests-run
|
||||||
|
- web-tests-skip
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Finalize Web Tests status
|
||||||
|
env:
|
||||||
|
SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
|
||||||
|
TESTS_CHANGED: ${{ needs.check-changes.outputs.web-changed }}
|
||||||
|
RUN_RESULT: ${{ needs.web-tests-run.result }}
|
||||||
|
SKIP_RESULT: ${{ needs.web-tests-skip.result }}
|
||||||
|
run: |
|
||||||
|
if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
|
||||||
|
echo "Web tests were skipped because this workflow run duplicated a successful or newer run."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$TESTS_CHANGED" == 'true' ]]; then
|
||||||
|
if [[ "$RUN_RESULT" == 'success' ]]; then
|
||||||
|
echo "Web tests ran successfully."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Web tests were required but finished with result: $RUN_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SKIP_RESULT" == 'success' ]]; then
|
||||||
|
echo "Web tests were skipped because no web-related files changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Web tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
web-e2e-run:
|
||||||
|
name: Run Web Full-Stack E2E
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.e2e-changed == 'true'
|
||||||
|
uses: ./.github/workflows/web-e2e.yml
|
||||||
|
|
||||||
|
web-e2e-skip:
|
||||||
|
name: Skip Web Full-Stack E2E
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.e2e-changed != 'true'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Report skipped web full-stack e2e
|
||||||
|
run: echo "No E2E-related changes detected; skipping web full-stack E2E."
|
||||||
|
|
||||||
|
web-e2e:
|
||||||
|
name: Web Full-Stack E2E
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
- web-e2e-run
|
||||||
|
- web-e2e-skip
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Finalize Web Full-Stack E2E status
|
||||||
|
env:
|
||||||
|
SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
|
||||||
|
TESTS_CHANGED: ${{ needs.check-changes.outputs.e2e-changed }}
|
||||||
|
RUN_RESULT: ${{ needs.web-e2e-run.result }}
|
||||||
|
SKIP_RESULT: ${{ needs.web-e2e-skip.result }}
|
||||||
|
run: |
|
||||||
|
if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
|
||||||
|
echo "Web full-stack E2E was skipped because this workflow run duplicated a successful or newer run."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$TESTS_CHANGED" == 'true' ]]; then
|
||||||
|
if [[ "$RUN_RESULT" == 'success' ]]; then
|
||||||
|
echo "Web full-stack E2E ran successfully."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Web full-stack E2E was required but finished with result: $RUN_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SKIP_RESULT" == 'success' ]]; then
|
||||||
|
echo "Web full-stack E2E was skipped because no E2E-related files changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Web full-stack E2E was not required, but the skip job finished with result: $SKIP_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
|
||||||
style-check:
|
style-check:
|
||||||
name: Style Check
|
name: Style Check
|
||||||
|
needs: pre_job
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true'
|
||||||
uses: ./.github/workflows/style.yml
|
uses: ./.github/workflows/style.yml
|
||||||
|
|
||||||
|
vdb-tests-run:
|
||||||
|
name: Run VDB Tests
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.vdb-changed == 'true'
|
||||||
|
uses: ./.github/workflows/vdb-tests.yml
|
||||||
|
|
||||||
|
vdb-tests-skip:
|
||||||
|
name: Skip VDB Tests
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.vdb-changed != 'true'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Report skipped VDB tests
|
||||||
|
run: echo "No VDB-related changes detected; skipping VDB tests."
|
||||||
|
|
||||||
vdb-tests:
|
vdb-tests:
|
||||||
name: VDB Tests
|
name: VDB Tests
|
||||||
needs: check-changes
|
if: ${{ always() }}
|
||||||
if: needs.check-changes.outputs.vdb-changed == 'true'
|
needs:
|
||||||
uses: ./.github/workflows/vdb-tests.yml
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
- vdb-tests-run
|
||||||
|
- vdb-tests-skip
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Finalize VDB Tests status
|
||||||
|
env:
|
||||||
|
SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
|
||||||
|
TESTS_CHANGED: ${{ needs.check-changes.outputs.vdb-changed }}
|
||||||
|
RUN_RESULT: ${{ needs.vdb-tests-run.result }}
|
||||||
|
SKIP_RESULT: ${{ needs.vdb-tests-skip.result }}
|
||||||
|
run: |
|
||||||
|
if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
|
||||||
|
echo "VDB tests were skipped because this workflow run duplicated a successful or newer run."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$TESTS_CHANGED" == 'true' ]]; then
|
||||||
|
if [[ "$RUN_RESULT" == 'success' ]]; then
|
||||||
|
echo "VDB tests ran successfully."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "VDB tests were required but finished with result: $RUN_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SKIP_RESULT" == 'success' ]]; then
|
||||||
|
echo "VDB tests were skipped because no VDB-related files changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "VDB tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
db-migration-test-run:
|
||||||
|
name: Run DB Migration Test
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.migration-changed == 'true'
|
||||||
|
uses: ./.github/workflows/db-migration-test.yml
|
||||||
|
|
||||||
|
db-migration-test-skip:
|
||||||
|
name: Skip DB Migration Test
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.migration-changed != 'true'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Report skipped DB migration tests
|
||||||
|
run: echo "No migration-related changes detected; skipping DB migration tests."
|
||||||
|
|
||||||
db-migration-test:
|
db-migration-test:
|
||||||
name: DB Migration Test
|
name: DB Migration Test
|
||||||
needs: check-changes
|
if: ${{ always() }}
|
||||||
if: needs.check-changes.outputs.migration-changed == 'true'
|
needs:
|
||||||
uses: ./.github/workflows/db-migration-test.yml
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
- db-migration-test-run
|
||||||
|
- db-migration-test-skip
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Finalize DB Migration Test status
|
||||||
|
env:
|
||||||
|
SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
|
||||||
|
TESTS_CHANGED: ${{ needs.check-changes.outputs.migration-changed }}
|
||||||
|
RUN_RESULT: ${{ needs.db-migration-test-run.result }}
|
||||||
|
SKIP_RESULT: ${{ needs.db-migration-test-skip.result }}
|
||||||
|
run: |
|
||||||
|
if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
|
||||||
|
echo "DB migration tests were skipped because this workflow run duplicated a successful or newer run."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$TESTS_CHANGED" == 'true' ]]; then
|
||||||
|
if [[ "$RUN_RESULT" == 'success' ]]; then
|
||||||
|
echo "DB migration tests ran successfully."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "DB migration tests were required but finished with result: $RUN_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SKIP_RESULT" == 'success' ]]; then
|
||||||
|
echo "DB migration tests were skipped because no migration-related files changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "DB migration tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
|||||||
40
.github/workflows/pyrefly-diff-comment.yml
vendored
40
.github/workflows/pyrefly-diff-comment.yml
vendored
@ -12,7 +12,7 @@ permissions: {}
|
|||||||
jobs:
|
jobs:
|
||||||
comment:
|
comment:
|
||||||
name: Comment PR with pyrefly diff
|
name: Comment PR with pyrefly diff
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
@ -21,7 +21,7 @@ jobs:
|
|||||||
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].head.repo.full_name != github.repository }}
|
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].head.repo.full_name != github.repository }}
|
||||||
steps:
|
steps:
|
||||||
- name: Download pyrefly diff artifact
|
- name: Download pyrefly diff artifact
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
@ -49,7 +49,7 @@ jobs:
|
|||||||
run: unzip -o pyrefly_diff.zip
|
run: unzip -o pyrefly_diff.zip
|
||||||
|
|
||||||
- name: Post comment
|
- name: Post comment
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
@ -76,13 +76,29 @@ jobs:
|
|||||||
diff += '\\n\\n... (truncated) ...';
|
diff += '\\n\\n... (truncated) ...';
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = diff.trim()
|
if (diff.trim()) {
|
||||||
? '### Pyrefly Diff\n<details>\n<summary>base → PR</summary>\n\n```diff\n' + diff + '\n```\n</details>'
|
const body = '### Pyrefly Diff\n<details>\n<summary>base → PR</summary>\n\n```diff\n' + diff + '\n```\n</details>';
|
||||||
: '### Pyrefly Diff\nNo changes detected.';
|
const marker = '### Pyrefly Diff';
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
});
|
||||||
|
const existing = comments.find((comment) => comment.body.startsWith(marker));
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
if (existing) {
|
||||||
issue_number: prNumber,
|
await github.rest.issues.updateComment({
|
||||||
owner: context.repo.owner,
|
comment_id: existing.id,
|
||||||
repo: context.repo.repo,
|
owner: context.repo.owner,
|
||||||
body,
|
repo: context.repo.repo,
|
||||||
});
|
body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
42
.github/workflows/pyrefly-diff.yml
vendored
42
.github/workflows/pyrefly-diff.yml
vendored
@ -10,7 +10,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pyrefly-diff:
|
pyrefly-diff:
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
issues: write
|
||||||
@ -22,7 +22,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Python & UV
|
- name: Setup Python & UV
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|
||||||
@ -50,12 +50,23 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
diff -u /tmp/pyrefly_base.txt /tmp/pyrefly_pr.txt > pyrefly_diff.txt || true
|
diff -u /tmp/pyrefly_base.txt /tmp/pyrefly_pr.txt > pyrefly_diff.txt || true
|
||||||
|
|
||||||
|
- name: Check if line counts match
|
||||||
|
id: line_count_check
|
||||||
|
run: |
|
||||||
|
base_lines=$(wc -l < /tmp/pyrefly_base.txt)
|
||||||
|
pr_lines=$(wc -l < /tmp/pyrefly_pr.txt)
|
||||||
|
if [ "$base_lines" -eq "$pr_lines" ]; then
|
||||||
|
echo "same=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "same=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Save PR number
|
- name: Save PR number
|
||||||
run: |
|
run: |
|
||||||
echo ${{ github.event.pull_request.number }} > pr_number.txt
|
echo ${{ github.event.pull_request.number }} > pr_number.txt
|
||||||
|
|
||||||
- name: Upload pyrefly diff
|
- name: Upload pyrefly diff
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: pyrefly_diff
|
name: pyrefly_diff
|
||||||
path: |
|
path: |
|
||||||
@ -63,8 +74,8 @@ jobs:
|
|||||||
pr_number.txt
|
pr_number.txt
|
||||||
|
|
||||||
- name: Comment PR with pyrefly diff
|
- name: Comment PR with pyrefly diff
|
||||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && steps.line_count_check.outputs.same == 'false' }}
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
@ -92,9 +103,26 @@ jobs:
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
: '### Pyrefly Diff\nNo changes detected.';
|
: '### Pyrefly Diff\nNo changes detected.';
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
const marker = '### Pyrefly Diff';
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
issue_number: prNumber,
|
issue_number: prNumber,
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
body,
|
|
||||||
});
|
});
|
||||||
|
const existing = comments.find((comment) => comment.body.startsWith(marker));
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
comment_id: existing.id,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
118
.github/workflows/pyrefly-type-coverage-comment.yml
vendored
Normal file
118
.github/workflows/pyrefly-type-coverage-comment.yml
vendored
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
name: Comment with Pyrefly Type Coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- Pyrefly Type Coverage
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment:
|
||||||
|
name: Comment PR with type coverage
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].head.repo.full_name != github.repository }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout default branch (trusted code)
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Setup Python & UV
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
|
- name: Download type coverage artifact
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: ${{ github.event.workflow_run.id }},
|
||||||
|
});
|
||||||
|
const match = artifacts.data.artifacts.find((artifact) =>
|
||||||
|
artifact.name === 'pyrefly_type_coverage'
|
||||||
|
);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error('pyrefly_type_coverage artifact not found');
|
||||||
|
}
|
||||||
|
const download = await github.rest.actions.downloadArtifact({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
artifact_id: match.id,
|
||||||
|
archive_format: 'zip',
|
||||||
|
});
|
||||||
|
fs.writeFileSync('pyrefly_type_coverage.zip', Buffer.from(download.data));
|
||||||
|
|
||||||
|
- name: Unzip artifact
|
||||||
|
run: unzip -o pyrefly_type_coverage.zip
|
||||||
|
|
||||||
|
- name: Render coverage markdown from structured data
|
||||||
|
id: render
|
||||||
|
run: |
|
||||||
|
comment_body="$(uv run --directory api python libs/pyrefly_type_coverage.py \
|
||||||
|
--base "$GITHUB_WORKSPACE/base_report.json" \
|
||||||
|
< "$GITHUB_WORKSPACE/pr_report.json")"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Pyrefly Type Coverage"
|
||||||
|
echo ""
|
||||||
|
echo "$comment_body"
|
||||||
|
} > /tmp/type_coverage_comment.md
|
||||||
|
|
||||||
|
- name: Post comment
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const body = fs.readFileSync('/tmp/type_coverage_comment.md', { encoding: 'utf8' });
|
||||||
|
let prNumber = null;
|
||||||
|
try {
|
||||||
|
prNumber = parseInt(fs.readFileSync('pr_number.txt', { encoding: 'utf8' }), 10);
|
||||||
|
} catch (err) {
|
||||||
|
const prs = context.payload.workflow_run.pull_requests || [];
|
||||||
|
if (prs.length > 0 && prs[0].number) {
|
||||||
|
prNumber = prs[0].number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!prNumber) {
|
||||||
|
throw new Error('PR number not found in artifact or workflow_run payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing comment if one exists, otherwise create new
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
});
|
||||||
|
const marker = '### Pyrefly Type Coverage';
|
||||||
|
const existing = comments.find(c => c.body.startsWith(marker));
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
comment_id: existing.id,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
124
.github/workflows/pyrefly-type-coverage.yml
vendored
Normal file
124
.github/workflows/pyrefly-type-coverage.yml
vendored
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
name: Pyrefly Type Coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'api/**/*.py'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pyrefly-type-coverage:
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout PR branch
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Python & UV
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
|
- name: Run pyrefly report on PR branch
|
||||||
|
run: |
|
||||||
|
uv run --directory api --dev pyrefly report 2>/dev/null > /tmp/pyrefly_report_pr.tmp && \
|
||||||
|
mv /tmp/pyrefly_report_pr.tmp /tmp/pyrefly_report_pr.json || \
|
||||||
|
echo '{}' > /tmp/pyrefly_report_pr.json
|
||||||
|
|
||||||
|
- name: Save helper script from base branch
|
||||||
|
run: |
|
||||||
|
git show ${{ github.event.pull_request.base.sha }}:api/libs/pyrefly_type_coverage.py > /tmp/pyrefly_type_coverage.py 2>/dev/null \
|
||||||
|
|| cp api/libs/pyrefly_type_coverage.py /tmp/pyrefly_type_coverage.py
|
||||||
|
|
||||||
|
- name: Checkout base branch
|
||||||
|
run: git checkout ${{ github.base_ref }}
|
||||||
|
|
||||||
|
- name: Run pyrefly report on base branch
|
||||||
|
run: |
|
||||||
|
uv run --directory api --dev pyrefly report 2>/dev/null > /tmp/pyrefly_report_base.tmp && \
|
||||||
|
mv /tmp/pyrefly_report_base.tmp /tmp/pyrefly_report_base.json || \
|
||||||
|
echo '{}' > /tmp/pyrefly_report_base.json
|
||||||
|
|
||||||
|
- name: Generate coverage comparison
|
||||||
|
id: coverage
|
||||||
|
run: |
|
||||||
|
comment_body="$(uv run --directory api python /tmp/pyrefly_type_coverage.py \
|
||||||
|
--base /tmp/pyrefly_report_base.json \
|
||||||
|
< /tmp/pyrefly_report_pr.json)"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Pyrefly Type Coverage"
|
||||||
|
echo ""
|
||||||
|
echo "$comment_body"
|
||||||
|
} | tee -a "$GITHUB_STEP_SUMMARY" > /tmp/type_coverage_comment.md
|
||||||
|
|
||||||
|
# Save structured data for the fork-PR comment workflow
|
||||||
|
cp /tmp/pyrefly_report_pr.json pr_report.json
|
||||||
|
cp /tmp/pyrefly_report_base.json base_report.json
|
||||||
|
# Keep fork-PR comments correct while the trusted workflow_run job is
|
||||||
|
# still using the default-branch renderer, which resolves --base from api/.
|
||||||
|
cp /tmp/pyrefly_report_base.json api/base_report.json
|
||||||
|
|
||||||
|
- name: Save PR number
|
||||||
|
run: |
|
||||||
|
echo ${{ github.event.pull_request.number }} > pr_number.txt
|
||||||
|
|
||||||
|
- name: Upload type coverage artifact
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: pyrefly_type_coverage
|
||||||
|
path: |
|
||||||
|
pr_report.json
|
||||||
|
base_report.json
|
||||||
|
api/base_report.json
|
||||||
|
pr_number.txt
|
||||||
|
|
||||||
|
- name: Comment PR with type coverage
|
||||||
|
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const marker = '### Pyrefly Type Coverage';
|
||||||
|
let body;
|
||||||
|
try {
|
||||||
|
body = fs.readFileSync('/tmp/type_coverage_comment.md', { encoding: 'utf8' });
|
||||||
|
} catch {
|
||||||
|
body = `${marker}\n\n_Coverage report unavailable._`;
|
||||||
|
}
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
|
||||||
|
// Update existing comment if one exists, otherwise create new
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
});
|
||||||
|
const existing = comments.find(c => c.body.startsWith(marker));
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
comment_id: existing.id,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
9
.github/workflows/semantic-pull-request.yml
vendored
9
.github/workflows/semantic-pull-request.yml
vendored
@ -7,15 +7,22 @@ on:
|
|||||||
- edited
|
- edited
|
||||||
- reopened
|
- reopened
|
||||||
- synchronize
|
- synchronize
|
||||||
|
merge_group:
|
||||||
|
branches: ["main"]
|
||||||
|
types: [checks_requested]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Validate PR title
|
name: Validate PR title
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
|
- name: Complete merge group check
|
||||||
|
if: github.event_name == 'merge_group'
|
||||||
|
run: echo "Semantic PR title validation is handled on pull requests."
|
||||||
- name: Check title
|
- name: Check title
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
8
.github/workflows/stale.yml
vendored
8
.github/workflows/stale.yml
vendored
@ -12,7 +12,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
@ -23,8 +23,8 @@ jobs:
|
|||||||
days-before-issue-stale: 15
|
days-before-issue-stale: 15
|
||||||
days-before-issue-close: 3
|
days-before-issue-close: 3
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
stale-issue-message: "Close due to it's no longer active, if you have any questions, you can reopen it."
|
stale-issue-message: "Closed due to inactivity. If you have any questions, you can reopen it."
|
||||||
stale-pr-message: "Close due to it's no longer active, if you have any questions, you can reopen it."
|
stale-pr-message: "Closed due to inactivity. If you have any questions, you can reopen it."
|
||||||
stale-issue-label: 'no-issue-activity'
|
stale-issue-label: 'no-issue-activity'
|
||||||
stale-pr-label: 'no-pr-activity'
|
stale-pr-label: 'no-pr-activity'
|
||||||
any-of-labels: 'duplicate,question,invalid,wontfix,no-issue-activity,no-pr-activity,enhancement,cant-reproduce,help-wanted'
|
any-of-labels: '🌚 invalid,🙋♂️ question,wont-fix,no-issue-activity,no-pr-activity,💪 enhancement,🤔 cant-reproduce,🙏 help wanted'
|
||||||
|
|||||||
47
.github/workflows/style.yml
vendored
47
.github/workflows/style.yml
vendored
@ -15,7 +15,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
python-style:
|
python-style:
|
||||||
name: Python Style
|
name: Python Style
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@ -25,7 +25,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check changed files
|
- name: Check changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
api/**
|
api/**
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: false
|
enable-cache: false
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
@ -47,9 +47,13 @@ jobs:
|
|||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: uv run --directory api --dev lint-imports
|
run: uv run --directory api --dev lint-imports
|
||||||
|
|
||||||
|
- name: Run Response Contract Linter
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: uv run --project api --dev python api/dev/lint_response_contracts.py --fail-on-mismatch
|
||||||
|
|
||||||
- name: Run Type Checks
|
- name: Run Type Checks
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: make type-check
|
run: make type-check-core
|
||||||
|
|
||||||
- name: Dotenv check
|
- name: Dotenv check
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@ -57,7 +61,7 @@ jobs:
|
|||||||
|
|
||||||
web-style:
|
web-style:
|
||||||
name: Web Style
|
name: Web Style
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
@ -73,10 +77,17 @@ jobs:
|
|||||||
|
|
||||||
- name: Check changed files
|
- name: Check changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
web/**
|
web/**
|
||||||
|
e2e/**
|
||||||
|
sdks/nodejs-client/**
|
||||||
|
packages/**
|
||||||
|
package.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
pnpm-workspace.yaml
|
||||||
|
.nvmrc
|
||||||
.github/workflows/style.yml
|
.github/workflows/style.yml
|
||||||
.github/actions/setup-web/**
|
.github/actions/setup-web/**
|
||||||
|
|
||||||
@ -87,26 +98,28 @@ jobs:
|
|||||||
- name: Restore ESLint cache
|
- name: Restore ESLint cache
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
id: eslint-cache-restore
|
id: eslint-cache-restore
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: web/.eslintcache
|
path: .eslintcache
|
||||||
key: ${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'web/pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }}
|
key: ${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'web/pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-
|
${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-
|
||||||
|
|
||||||
- name: Web style check
|
- name: Web style check
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
working-directory: ./web
|
working-directory: .
|
||||||
run: vp run lint:ci
|
run: vp run lint:ci
|
||||||
|
|
||||||
- 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:
|
||||||
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
run: vp run lint:tss
|
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: ./web
|
working-directory: .
|
||||||
run: vp run type-check
|
run: vp run type-check
|
||||||
|
|
||||||
- name: Web dead code check
|
- name: Web dead code check
|
||||||
@ -116,14 +129,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Save ESLint cache
|
- name: Save ESLint cache
|
||||||
if: steps.changed-files.outputs.any_changed == 'true' && success() && steps.eslint-cache-restore.outputs.cache-hit != 'true'
|
if: steps.changed-files.outputs.any_changed == 'true' && success() && steps.eslint-cache-restore.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: web/.eslintcache
|
path: .eslintcache
|
||||||
key: ${{ steps.eslint-cache-restore.outputs.cache-primary-key }}
|
key: ${{ steps.eslint-cache-restore.outputs.cache-primary-key }}
|
||||||
|
|
||||||
superlinter:
|
superlinter:
|
||||||
name: SuperLinter
|
name: SuperLinter
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@ -134,7 +147,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check changed files
|
- name: Check changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
**.sh
|
**.sh
|
||||||
@ -145,7 +158,7 @@ jobs:
|
|||||||
.editorconfig
|
.editorconfig
|
||||||
|
|
||||||
- name: Super-linter
|
- name: Super-linter
|
||||||
uses: super-linter/super-linter/slim@61abc07d755095a68f4987d1c2c3d1d64408f1f9 # v8.5.0
|
uses: super-linter/super-linter/slim@9e863354e3ff62e0727d37183162c4a88873df41 # v8.6.0
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
env:
|
env:
|
||||||
BASH_SEVERITY: warning
|
BASH_SEVERITY: warning
|
||||||
|
|||||||
7
.github/workflows/tool-test-sdks.yaml
vendored
7
.github/workflows/tool-test-sdks.yaml
vendored
@ -6,6 +6,9 @@ on:
|
|||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- sdks/**
|
- sdks/**
|
||||||
|
- package.json
|
||||||
|
- pnpm-lock.yaml
|
||||||
|
- pnpm-workspace.yaml
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: sdk-tests-${{ github.head_ref || github.run_id }}
|
group: sdk-tests-${{ github.head_ref || github.run_id }}
|
||||||
@ -14,7 +17,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: unit test for Node.js SDK
|
name: unit test for Node.js SDK
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@ -26,7 +29,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: ''
|
cache: ''
|
||||||
|
|||||||
643
.github/workflows/translate-i18n-claude.yml
vendored
643
.github/workflows/translate-i18n-claude.yml
vendored
@ -1,26 +1,24 @@
|
|||||||
name: Translate i18n Files with Claude Code
|
name: Translate i18n Files with Claude Code
|
||||||
|
|
||||||
# Note: claude-code-action doesn't support push events directly.
|
# Note: claude-code-action doesn't support push events directly.
|
||||||
# Push events are handled by trigger-i18n-sync.yml which sends repository_dispatch.
|
# Push events are bridged by trigger-i18n-sync.yml via repository_dispatch.
|
||||||
# See: https://github.com/langgenius/dify/issues/30743
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
repository_dispatch:
|
repository_dispatch:
|
||||||
types: [i18n-sync]
|
types: [i18n-sync]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
files:
|
files:
|
||||||
description: 'Specific files to translate (space-separated, e.g., "app common"). Leave empty for all files.'
|
description: 'Specific files to translate (space-separated, e.g., "app common"). Required for full mode; leave empty in incremental mode to use en-US files changed since HEAD~1.'
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
languages:
|
languages:
|
||||||
description: 'Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP"). Leave empty for all supported languages.'
|
description: 'Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP"). Leave empty for all supported target languages except en-US.'
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
mode:
|
mode:
|
||||||
description: 'Sync mode: incremental (only changes) or full (re-check all keys)'
|
description: 'Sync mode: incremental (compare with previous en-US revision) or full (sync all keys in scope)'
|
||||||
required: false
|
required: false
|
||||||
default: 'incremental'
|
default: incremental
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- incremental
|
- incremental
|
||||||
@ -30,11 +28,15 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: translate-i18n-${{ github.event_name }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
translate:
|
translate:
|
||||||
if: github.repository == 'langgenius/dify'
|
if: github.repository == 'langgenius/dify'
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
timeout-minutes: 60
|
timeout-minutes: 120
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@ -51,380 +53,293 @@ jobs:
|
|||||||
- name: Setup web environment
|
- name: Setup web environment
|
||||||
uses: ./.github/actions/setup-web
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
- name: Detect changed files and generate diff
|
- name: Prepare sync context
|
||||||
id: detect_changes
|
id: context
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
DEFAULT_TARGET_LANGS=$(awk "
|
||||||
# Manual trigger
|
/value: '/ {
|
||||||
if [ -n "${{ github.event.inputs.files }}" ]; then
|
value=\$2
|
||||||
echo "CHANGED_FILES=${{ github.event.inputs.files }}" >> $GITHUB_OUTPUT
|
gsub(/[',]/, \"\", value)
|
||||||
else
|
}
|
||||||
# Get all JSON files in en-US directory
|
/supported: true/ && value != \"en-US\" {
|
||||||
files=$(ls web/i18n/en-US/*.json 2>/dev/null | xargs -n1 basename | sed 's/.json$//' | tr '\n' ' ')
|
printf \"%s \", value
|
||||||
echo "CHANGED_FILES=$files" >> $GITHUB_OUTPUT
|
}
|
||||||
fi
|
" web/i18n-config/languages.ts | sed 's/[[:space:]]*$//')
|
||||||
echo "TARGET_LANGS=${{ github.event.inputs.languages }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "SYNC_MODE=${{ github.event.inputs.mode || 'incremental' }}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# For manual trigger with incremental mode, get diff from last commit
|
generate_changes_json() {
|
||||||
# For full mode, we'll do a complete check anyway
|
node .github/scripts/generate-i18n-changes.mjs
|
||||||
if [ "${{ github.event.inputs.mode }}" == "full" ]; then
|
}
|
||||||
echo "Full mode: will check all keys" > /tmp/i18n-diff.txt
|
|
||||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
git diff HEAD~1..HEAD -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || echo "" > /tmp/i18n-diff.txt
|
|
||||||
if [ -s /tmp/i18n-diff.txt ]; then
|
|
||||||
echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
|
|
||||||
# Triggered by push via trigger-i18n-sync.yml workflow
|
|
||||||
# Validate required payload fields
|
|
||||||
if [ -z "${{ github.event.client_payload.changed_files }}" ]; then
|
|
||||||
echo "Error: repository_dispatch payload missing required 'changed_files' field" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "CHANGED_FILES=${{ github.event.client_payload.changed_files }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "TARGET_LANGS=" >> $GITHUB_OUTPUT
|
|
||||||
echo "SYNC_MODE=${{ github.event.client_payload.sync_mode || 'incremental' }}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Decode the base64-encoded diff from the trigger workflow
|
if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
|
||||||
if [ -n "${{ github.event.client_payload.diff_base64 }}" ]; then
|
BASE_SHA="${{ github.event.client_payload.base_sha }}"
|
||||||
if ! echo "${{ github.event.client_payload.diff_base64 }}" | base64 -d > /tmp/i18n-diff.txt 2>&1; then
|
HEAD_SHA="${{ github.event.client_payload.head_sha }}"
|
||||||
echo "Warning: Failed to decode base64 diff payload" >&2
|
CHANGED_FILES="${{ github.event.client_payload.changed_files }}"
|
||||||
echo "" > /tmp/i18n-diff.txt
|
TARGET_LANGS="$DEFAULT_TARGET_LANGS"
|
||||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
SYNC_MODE="${{ github.event.client_payload.sync_mode || 'incremental' }}"
|
||||||
elif [ -s /tmp/i18n-diff.txt ]; then
|
|
||||||
echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT
|
if [ -n "${{ github.event.client_payload.changes_base64 }}" ]; then
|
||||||
else
|
printf '%s' '${{ github.event.client_payload.changes_base64 }}' | base64 -d > /tmp/i18n-changes.json
|
||||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
CHANGES_AVAILABLE="true"
|
||||||
fi
|
CHANGES_SOURCE="embedded"
|
||||||
|
elif [ -n "$BASE_SHA" ] && [ -n "$CHANGED_FILES" ]; then
|
||||||
|
export BASE_SHA HEAD_SHA CHANGED_FILES
|
||||||
|
generate_changes_json
|
||||||
|
CHANGES_AVAILABLE="true"
|
||||||
|
CHANGES_SOURCE="recomputed"
|
||||||
else
|
else
|
||||||
echo "" > /tmp/i18n-diff.txt
|
printf '%s' '{"baseSha":"","headSha":"","files":[],"changes":{}}' > /tmp/i18n-changes.json
|
||||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
CHANGES_AVAILABLE="false"
|
||||||
|
CHANGES_SOURCE="unavailable"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "Unsupported event type: ${{ github.event_name }}"
|
BASE_SHA=""
|
||||||
exit 1
|
HEAD_SHA=$(git rev-parse HEAD)
|
||||||
|
if [ -n "${{ github.event.inputs.languages }}" ]; then
|
||||||
|
TARGET_LANGS="${{ github.event.inputs.languages }}"
|
||||||
|
else
|
||||||
|
TARGET_LANGS="$DEFAULT_TARGET_LANGS"
|
||||||
|
fi
|
||||||
|
SYNC_MODE="${{ github.event.inputs.mode || 'incremental' }}"
|
||||||
|
if [ -n "${{ github.event.inputs.files }}" ]; then
|
||||||
|
CHANGED_FILES="${{ github.event.inputs.files }}"
|
||||||
|
elif [ "$SYNC_MODE" = "incremental" ]; then
|
||||||
|
BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || true)
|
||||||
|
if [ -n "$BASE_SHA" ]; then
|
||||||
|
CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' 2>/dev/null | sed -n 's@^.*/@@p' | sed 's/\.json$//' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||||
|
else
|
||||||
|
CHANGED_FILES=$(find web/i18n/en-US -maxdepth 1 -type f -name '*.json' -print | sed -n 's@^.*/@@p' | sed 's/\.json$//' | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||||
|
fi
|
||||||
|
elif [ "$SYNC_MODE" = "full" ]; then
|
||||||
|
echo "workflow_dispatch full mode requires the files input to stay within CI limits." >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
CHANGED_FILES=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$SYNC_MODE" = "incremental" ] && [ -n "$CHANGED_FILES" ]; then
|
||||||
|
export BASE_SHA HEAD_SHA CHANGED_FILES
|
||||||
|
generate_changes_json
|
||||||
|
CHANGES_AVAILABLE="true"
|
||||||
|
CHANGES_SOURCE="local"
|
||||||
|
else
|
||||||
|
printf '%s' '{"baseSha":"","headSha":"","files":[],"changes":{}}' > /tmp/i18n-changes.json
|
||||||
|
CHANGES_AVAILABLE="false"
|
||||||
|
CHANGES_SOURCE="unavailable"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Truncate diff if too large (keep first 50KB)
|
FILE_ARGS=""
|
||||||
if [ -f /tmp/i18n-diff.txt ]; then
|
if [ -n "$CHANGED_FILES" ]; then
|
||||||
head -c 50000 /tmp/i18n-diff.txt > /tmp/i18n-diff-truncated.txt
|
FILE_ARGS="--file $CHANGED_FILES"
|
||||||
mv /tmp/i18n-diff-truncated.txt /tmp/i18n-diff.txt
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Detected files: $(cat $GITHUB_OUTPUT | grep CHANGED_FILES || echo 'none')"
|
LANG_ARGS=""
|
||||||
|
if [ -n "$TARGET_LANGS" ]; then
|
||||||
|
LANG_ARGS="--lang $TARGET_LANGS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "DEFAULT_TARGET_LANGS=$DEFAULT_TARGET_LANGS"
|
||||||
|
echo "BASE_SHA=$BASE_SHA"
|
||||||
|
echo "HEAD_SHA=$HEAD_SHA"
|
||||||
|
echo "CHANGED_FILES=$CHANGED_FILES"
|
||||||
|
echo "TARGET_LANGS=$TARGET_LANGS"
|
||||||
|
echo "SYNC_MODE=$SYNC_MODE"
|
||||||
|
echo "CHANGES_AVAILABLE=$CHANGES_AVAILABLE"
|
||||||
|
echo "CHANGES_SOURCE=$CHANGES_SOURCE"
|
||||||
|
echo "FILE_ARGS=$FILE_ARGS"
|
||||||
|
echo "LANG_ARGS=$LANG_ARGS"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
echo "Files: ${CHANGED_FILES:-<none>}"
|
||||||
|
echo "Languages: ${TARGET_LANGS:-<none>}"
|
||||||
|
echo "Mode: $SYNC_MODE"
|
||||||
|
|
||||||
- name: Run Claude Code for Translation Sync
|
- name: Run Claude Code for Translation Sync
|
||||||
if: steps.detect_changes.outputs.CHANGED_FILES != ''
|
if: steps.context.outputs.CHANGED_FILES != ''
|
||||||
uses: anthropics/claude-code-action@ff9acae5886d41a99ed4ec14b7dc147d55834722 # v1.0.77
|
uses: anthropics/claude-code-action@1dc994ee7a008f0ecc866d9ac23ef036b7229f84 # v1.0.127
|
||||||
with:
|
with:
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# Allow github-actions bot to trigger this workflow via repository_dispatch
|
|
||||||
# See: https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
|
||||||
allowed_bots: 'github-actions[bot]'
|
allowed_bots: 'github-actions[bot]'
|
||||||
|
show_full_output: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
prompt: |
|
prompt: |
|
||||||
You are a professional i18n synchronization engineer for the Dify project.
|
You are the i18n sync agent for the Dify repository.
|
||||||
Your task is to keep all language translations in sync with the English source (en-US).
|
Your job is to keep translations synchronized with the English source files under `${{ github.workspace }}/web/i18n/en-US/`.
|
||||||
|
|
||||||
## CRITICAL TOOL RESTRICTIONS
|
Use absolute paths at all times:
|
||||||
- Use **Read** tool to read files (NOT cat or bash)
|
- Repo root: `${{ github.workspace }}`
|
||||||
- Use **Edit** tool to modify JSON files (NOT node, jq, or bash scripts)
|
- Web directory: `${{ github.workspace }}/web`
|
||||||
- Use **Bash** ONLY for: git commands, gh commands, pnpm commands
|
- Language config: `${{ github.workspace }}/web/i18n-config/languages.ts`
|
||||||
- Run bash commands ONE BY ONE, never combine with && or ||
|
|
||||||
- NEVER use `$()` command substitution - it's not supported. Split into separate commands instead.
|
|
||||||
|
|
||||||
## WORKING DIRECTORY & ABSOLUTE PATHS
|
Inputs:
|
||||||
Claude Code sandbox working directory may vary. Always use absolute paths:
|
- Files in scope: `${{ steps.context.outputs.CHANGED_FILES }}`
|
||||||
- For pnpm: `pnpm --dir ${{ github.workspace }}/web <command>`
|
- Target languages: `${{ steps.context.outputs.TARGET_LANGS }}`
|
||||||
- For git: `git -C ${{ github.workspace }} <command>`
|
- Sync mode: `${{ steps.context.outputs.SYNC_MODE }}`
|
||||||
- For gh: `gh --repo ${{ github.repository }} <command>`
|
- Base SHA: `${{ steps.context.outputs.BASE_SHA }}`
|
||||||
- For file paths: `${{ github.workspace }}/web/i18n/`
|
- Head SHA: `${{ steps.context.outputs.HEAD_SHA }}`
|
||||||
|
- Scoped file args: `${{ steps.context.outputs.FILE_ARGS }}`
|
||||||
|
- Scoped language args: `${{ steps.context.outputs.LANG_ARGS }}`
|
||||||
|
- Structured change set available: `${{ steps.context.outputs.CHANGES_AVAILABLE }}`
|
||||||
|
- Structured change set source: `${{ steps.context.outputs.CHANGES_SOURCE }}`
|
||||||
|
- Structured change set file: `/tmp/i18n-changes.json`
|
||||||
|
|
||||||
## EFFICIENCY RULES
|
Tool rules:
|
||||||
- **ONE Edit per language file** - batch all key additions into a single Edit
|
- Use Read for repository files.
|
||||||
- Insert new keys at the beginning of JSON (after `{`), lint:fix will sort them
|
- Use Edit for JSON updates.
|
||||||
- Translate ALL keys for a language mentally first, then do ONE Edit
|
- Use Bash only for `vp`.
|
||||||
|
- Do not use Bash for `git`, `gh`, or branch management.
|
||||||
## Context
|
|
||||||
- Changed/target files: ${{ steps.detect_changes.outputs.CHANGED_FILES }}
|
|
||||||
- Target languages (empty means all supported): ${{ steps.detect_changes.outputs.TARGET_LANGS }}
|
|
||||||
- Sync mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}
|
|
||||||
- Translation files are located in: ${{ github.workspace }}/web/i18n/{locale}/{filename}.json
|
|
||||||
- Language configuration is in: ${{ github.workspace }}/web/i18n-config/languages.ts
|
|
||||||
- Git diff is available: ${{ steps.detect_changes.outputs.DIFF_AVAILABLE }}
|
|
||||||
|
|
||||||
## CRITICAL DESIGN: Verify First, Then Sync
|
|
||||||
|
|
||||||
You MUST follow this three-phase approach:
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
║ PHASE 1: VERIFY - Analyze and Generate Change Report ║
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
### Step 1.1: Analyze Git Diff (for incremental mode)
|
|
||||||
Use the Read tool to read `/tmp/i18n-diff.txt` to see the git diff.
|
|
||||||
|
|
||||||
Parse the diff to categorize changes:
|
|
||||||
- Lines with `+` (not `+++`): Added or modified values
|
|
||||||
- Lines with `-` (not `---`): Removed or old values
|
|
||||||
- Identify specific keys for each category:
|
|
||||||
* ADD: Keys that appear only in `+` lines (new keys)
|
|
||||||
* UPDATE: Keys that appear in both `-` and `+` lines (value changed)
|
|
||||||
* DELETE: Keys that appear only in `-` lines (removed keys)
|
|
||||||
|
|
||||||
### Step 1.2: Read Language Configuration
|
|
||||||
Use the Read tool to read `${{ github.workspace }}/web/i18n-config/languages.ts`.
|
|
||||||
Extract all languages with `supported: true`.
|
|
||||||
|
|
||||||
### Step 1.3: Run i18n:check for Each Language
|
|
||||||
```bash
|
|
||||||
pnpm --dir ${{ github.workspace }}/web install --frozen-lockfile
|
|
||||||
```
|
|
||||||
```bash
|
|
||||||
pnpm --dir ${{ github.workspace }}/web run i18n:check
|
|
||||||
```
|
|
||||||
|
|
||||||
This will report:
|
|
||||||
- Missing keys (need to ADD)
|
|
||||||
- Extra keys (need to DELETE)
|
|
||||||
|
|
||||||
### Step 1.4: Generate Change Report
|
|
||||||
|
|
||||||
Create a structured report identifying:
|
|
||||||
```
|
|
||||||
╔══════════════════════════════════════════════════════════════╗
|
|
||||||
║ I18N SYNC CHANGE REPORT ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ Files to process: [list] ║
|
|
||||||
║ Languages to sync: [list] ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ ADD (New Keys): ║
|
|
||||||
║ - [filename].[key]: "English value" ║
|
|
||||||
║ ... ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ UPDATE (Modified Keys - MUST re-translate): ║
|
|
||||||
║ - [filename].[key]: "Old value" → "New value" ║
|
|
||||||
║ ... ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ DELETE (Extra Keys): ║
|
|
||||||
║ - [language]/[filename].[key] ║
|
|
||||||
║ ... ║
|
|
||||||
╚══════════════════════════════════════════════════════════════╝
|
|
||||||
```
|
|
||||||
|
|
||||||
**IMPORTANT**: For UPDATE detection, compare git diff to find keys where
|
|
||||||
the English value changed. These MUST be re-translated even if target
|
|
||||||
language already has a translation (it's now stale!).
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
║ PHASE 2: SYNC - Execute Changes Based on Report ║
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
### Step 2.1: Process ADD Operations (BATCH per language file)
|
|
||||||
|
|
||||||
**CRITICAL WORKFLOW for efficiency:**
|
|
||||||
1. First, translate ALL new keys for ALL languages mentally
|
|
||||||
2. Then, for EACH language file, do ONE Edit operation:
|
|
||||||
- Read the file once
|
|
||||||
- Insert ALL new keys at the beginning (right after the opening `{`)
|
|
||||||
- Don't worry about alphabetical order - lint:fix will sort them later
|
|
||||||
|
|
||||||
Example Edit (adding 3 keys to zh-Hans/app.json):
|
|
||||||
```
|
|
||||||
old_string: '{\n "accessControl"'
|
|
||||||
new_string: '{\n "newKey1": "translation1",\n "newKey2": "translation2",\n "newKey3": "translation3",\n "accessControl"'
|
|
||||||
```
|
|
||||||
|
|
||||||
**IMPORTANT**:
|
|
||||||
- ONE Edit per language file (not one Edit per key!)
|
|
||||||
- Always use the Edit tool. NEVER use bash scripts, node, or jq.
|
|
||||||
|
|
||||||
### Step 2.2: Process UPDATE Operations
|
|
||||||
|
|
||||||
**IMPORTANT: Special handling for zh-Hans and ja-JP**
|
|
||||||
If zh-Hans or ja-JP files were ALSO modified in the same push:
|
|
||||||
- Run: `git -C ${{ github.workspace }} diff HEAD~1 --name-only` and check for zh-Hans or ja-JP files
|
|
||||||
- If found, it means someone manually translated them. Apply these rules:
|
|
||||||
|
|
||||||
1. **Missing keys**: Still ADD them (completeness required)
|
|
||||||
2. **Existing translations**: Compare with the NEW English value:
|
|
||||||
- If translation is **completely wrong** or **unrelated** → Update it
|
|
||||||
- If translation is **roughly correct** (captures the meaning) → Keep it, respect manual work
|
|
||||||
- When in doubt, **keep the manual translation**
|
|
||||||
|
|
||||||
Example:
|
|
||||||
- English changed: "Save" → "Save Changes"
|
|
||||||
- Manual translation: "保存更改" → Keep it (correct meaning)
|
|
||||||
- Manual translation: "删除" → Update it (completely wrong)
|
|
||||||
|
|
||||||
For other languages:
|
|
||||||
Use Edit tool to replace the old value with the new translation.
|
|
||||||
You can batch multiple updates in one Edit if they are adjacent.
|
|
||||||
|
|
||||||
### Step 2.3: Process DELETE Operations
|
|
||||||
For extra keys reported by i18n:check:
|
|
||||||
- Run: `pnpm --dir ${{ github.workspace }}/web run i18n:check --auto-remove`
|
|
||||||
- Or manually remove from target language JSON files
|
|
||||||
|
|
||||||
## Translation Guidelines
|
|
||||||
|
|
||||||
- PRESERVE all placeholders exactly as-is:
|
|
||||||
- `{{variable}}` - Mustache interpolation
|
|
||||||
- `${variable}` - Template literal
|
|
||||||
- `<tag>content</tag>` - HTML tags
|
|
||||||
- `_one`, `_other` - Pluralization suffixes (these are KEY suffixes, not values)
|
|
||||||
|
|
||||||
**CRITICAL: Variable names and tag names MUST stay in English - NEVER translate them**
|
|
||||||
|
|
||||||
✅ CORRECT examples:
|
|
||||||
- English: "{{count}} items" → Japanese: "{{count}} 個のアイテム"
|
|
||||||
- English: "{{name}} updated" → Korean: "{{name}} 업데이트됨"
|
|
||||||
- English: "<email>{{email}}</email>" → Chinese: "<email>{{email}}</email>"
|
|
||||||
- English: "<CustomLink>Marketplace</CustomLink>" → Japanese: "<CustomLink>マーケットプレイス</CustomLink>"
|
|
||||||
|
|
||||||
❌ WRONG examples (NEVER do this - will break the application):
|
|
||||||
- "{{count}}" → "{{カウント}}" ❌ (variable name translated to Japanese)
|
|
||||||
- "{{name}}" → "{{이름}}" ❌ (variable name translated to Korean)
|
|
||||||
- "{{email}}" → "{{邮箱}}" ❌ (variable name translated to Chinese)
|
|
||||||
- "<email>" → "<メール>" ❌ (tag name translated)
|
|
||||||
- "<CustomLink>" → "<自定义链接>" ❌ (component name translated)
|
|
||||||
|
|
||||||
- Use appropriate language register (formal/informal) based on existing translations
|
|
||||||
- Match existing translation style in each language
|
|
||||||
- Technical terms: check existing conventions per language
|
|
||||||
- For CJK languages: no spaces between characters unless necessary
|
|
||||||
- For RTL languages (ar-TN, fa-IR): ensure proper text handling
|
|
||||||
|
|
||||||
## Output Format Requirements
|
|
||||||
- Alphabetical key ordering (if original file uses it)
|
|
||||||
- 2-space indentation
|
|
||||||
- Trailing newline at end of file
|
|
||||||
- Valid JSON (use proper escaping for special characters)
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
║ PHASE 3: RE-VERIFY - Confirm All Issues Resolved ║
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
### Step 3.1: Run Lint Fix (IMPORTANT!)
|
|
||||||
```bash
|
|
||||||
pnpm --dir ${{ github.workspace }}/web lint:fix --quiet -- 'i18n/**/*.json'
|
|
||||||
```
|
|
||||||
This ensures:
|
|
||||||
- JSON keys are sorted alphabetically (jsonc/sort-keys rule)
|
|
||||||
- Valid i18n keys (dify-i18n/valid-i18n-keys rule)
|
|
||||||
- No extra keys (dify-i18n/no-extra-keys rule)
|
|
||||||
|
|
||||||
### Step 3.2: Run Final i18n Check
|
|
||||||
```bash
|
|
||||||
pnpm --dir ${{ github.workspace }}/web run i18n:check
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3.3: Fix Any Remaining Issues
|
|
||||||
If check reports issues:
|
|
||||||
- Go back to PHASE 2 for unresolved items
|
|
||||||
- Repeat until check passes
|
|
||||||
|
|
||||||
### Step 3.4: Generate Final Summary
|
|
||||||
```
|
|
||||||
╔══════════════════════════════════════════════════════════════╗
|
|
||||||
║ SYNC COMPLETED SUMMARY ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ Language │ Added │ Updated │ Deleted │ Status ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ zh-Hans │ 5 │ 2 │ 1 │ ✓ Complete ║
|
|
||||||
║ ja-JP │ 5 │ 2 │ 1 │ ✓ Complete ║
|
|
||||||
║ ... │ ... │ ... │ ... │ ... ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ i18n:check │ PASSED - All keys in sync ║
|
|
||||||
╚══════════════════════════════════════════════════════════════╝
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mode-Specific Behavior
|
|
||||||
|
|
||||||
**SYNC_MODE = "incremental"** (default):
|
|
||||||
- Focus on keys identified from git diff
|
|
||||||
- Also check i18n:check output for any missing/extra keys
|
|
||||||
- Efficient for small changes
|
|
||||||
|
|
||||||
**SYNC_MODE = "full"**:
|
|
||||||
- Compare ALL keys between en-US and each language
|
|
||||||
- Run i18n:check to identify all discrepancies
|
|
||||||
- Use for first-time sync or fixing historical issues
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
1. Always run i18n:check BEFORE and AFTER making changes
|
|
||||||
2. The check script is the source of truth for missing/extra keys
|
|
||||||
3. For UPDATE scenario: git diff is the source of truth for changed values
|
|
||||||
4. Create a single commit with all translation changes
|
|
||||||
5. If any translation fails, continue with others and report failures
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
║ PHASE 4: COMMIT AND CREATE PR ║
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
After all translations are complete and verified:
|
|
||||||
|
|
||||||
### Step 4.1: Check for changes
|
|
||||||
```bash
|
|
||||||
git -C ${{ github.workspace }} status --porcelain
|
|
||||||
```
|
|
||||||
|
|
||||||
If there are changes:
|
|
||||||
|
|
||||||
### Step 4.2: Create a new branch and commit
|
|
||||||
Run these git commands ONE BY ONE (not combined with &&).
|
|
||||||
**IMPORTANT**: Do NOT use `$()` command substitution. Use two separate commands:
|
|
||||||
|
|
||||||
1. First, get the timestamp:
|
|
||||||
```bash
|
|
||||||
date +%Y%m%d-%H%M%S
|
|
||||||
```
|
|
||||||
(Note the output, e.g., "20260115-143052")
|
|
||||||
|
|
||||||
2. Then create branch using the timestamp value:
|
|
||||||
```bash
|
|
||||||
git -C ${{ github.workspace }} checkout -b chore/i18n-sync-20260115-143052
|
|
||||||
```
|
|
||||||
(Replace "20260115-143052" with the actual timestamp from step 1)
|
|
||||||
|
|
||||||
3. Stage changes:
|
|
||||||
```bash
|
|
||||||
git -C ${{ github.workspace }} add web/i18n/
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Commit:
|
|
||||||
```bash
|
|
||||||
git -C ${{ github.workspace }} commit -m "chore(i18n): sync translations with en-US - Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Push:
|
|
||||||
```bash
|
|
||||||
git -C ${{ github.workspace }} push origin HEAD
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4.3: Create Pull Request
|
|
||||||
```bash
|
|
||||||
gh pr create --repo ${{ github.repository }} --title "chore(i18n): sync translations with en-US" --body "## Summary
|
|
||||||
|
|
||||||
This PR was automatically generated to sync i18n translation files.
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
- Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}
|
|
||||||
- Files processed: ${{ steps.detect_changes.outputs.CHANGED_FILES }}
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
- [x] \`i18n:check\` passed
|
|
||||||
- [x] \`lint:fix\` applied
|
|
||||||
|
|
||||||
🤖 Generated with Claude Code GitHub Action" --base main
|
|
||||||
```
|
|
||||||
|
|
||||||
|
Required execution plan:
|
||||||
|
1. Resolve target languages.
|
||||||
|
- Use the provided `Target languages` value as the source of truth.
|
||||||
|
- If it is unexpectedly empty, read `${{ github.workspace }}/web/i18n-config/languages.ts` and use every language with `supported: true` except `en-US`.
|
||||||
|
2. Stay strictly in scope.
|
||||||
|
- Only process the files listed in `Files in scope`.
|
||||||
|
- Only process the resolved target languages, never `en-US`.
|
||||||
|
- Do not touch unrelated i18n files.
|
||||||
|
- Do not modify `${{ github.workspace }}/web/i18n/en-US/`.
|
||||||
|
3. Resolve source changes.
|
||||||
|
- If `Structured change set available` is `true`, read `/tmp/i18n-changes.json` and use it as the source of truth for file-level and key-level changes.
|
||||||
|
- For each file entry:
|
||||||
|
- `added` contains new English keys that need translations.
|
||||||
|
- `updated` contains stale keys whose English source changed; re-translate using the `after` value.
|
||||||
|
- `deleted` contains keys that should be removed from locale files.
|
||||||
|
- `fileDeleted: true` means the English file no longer exists; remove the matching locale file if present.
|
||||||
|
- Read the current English JSON file for any file that still exists so wording, placeholders, and surrounding terminology stay accurate.
|
||||||
|
- If `Structured change set available` is `false`, treat this as a scoped full sync and use the current English files plus scoped checks as the source of truth.
|
||||||
|
4. Run a scoped pre-check before editing:
|
||||||
|
- `vp run dify-web#i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }}`
|
||||||
|
- Use this command as the source of truth for missing and extra keys inside the current scope.
|
||||||
|
5. Apply translations.
|
||||||
|
- For every target language and scoped file:
|
||||||
|
- If `fileDeleted` is `true`, remove the locale file if it exists and skip the rest of that file.
|
||||||
|
- If the locale file does not exist yet, create it with `Write` and then continue with `Edit` as needed.
|
||||||
|
- ADD missing keys.
|
||||||
|
- UPDATE stale translations when the English value changed.
|
||||||
|
- DELETE removed keys. Prefer `vp run dify-web#i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }} --auto-remove` for extra keys so deletions stay in scope.
|
||||||
|
- Preserve placeholders exactly: `{{variable}}`, `${variable}`, HTML tags, component tags, and variable names.
|
||||||
|
- Match the existing terminology and register used by each locale.
|
||||||
|
- Prefer one Edit per file when stable, but prioritize correctness over batching.
|
||||||
|
6. Verify only the edited files.
|
||||||
|
- Run `vp run dify-web#lint:fix --quiet -- <relative edited i18n file paths under web/>`
|
||||||
|
- Run `vp run dify-web#i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }}`
|
||||||
|
- If verification fails, fix the remaining problems before continuing.
|
||||||
|
7. Stop after the scoped locale files are updated and verification passes.
|
||||||
|
- Do not create branches, commits, or pull requests.
|
||||||
claude_args: |
|
claude_args: |
|
||||||
--max-turns 150
|
--max-turns 120
|
||||||
--allowedTools "Read,Write,Edit,Bash(git *),Bash(git:*),Bash(gh *),Bash(gh:*),Bash(pnpm *),Bash(pnpm:*),Bash(date *),Bash(date:*),Glob,Grep"
|
--allowedTools "Read,Write,Edit,Bash(vp *),Bash(vp:*),Glob,Grep"
|
||||||
|
|
||||||
|
- name: Prepare branch metadata
|
||||||
|
id: pr_meta
|
||||||
|
if: steps.context.outputs.CHANGED_FILES != ''
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ -z "$(git -C "${{ github.workspace }}" status --porcelain -- web/i18n/)" ]; then
|
||||||
|
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCOPE_HASH=$(printf '%s|%s|%s' "${{ steps.context.outputs.CHANGED_FILES }}" "${{ steps.context.outputs.TARGET_LANGS }}" "${{ steps.context.outputs.SYNC_MODE }}" | sha256sum | cut -c1-8)
|
||||||
|
HEAD_SHORT=$(printf '%s' "${{ steps.context.outputs.HEAD_SHA }}" | cut -c1-12)
|
||||||
|
BRANCH_NAME="chore/i18n-sync-${HEAD_SHORT}-${SCOPE_HASH}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "has_changes=true"
|
||||||
|
echo "branch_name=$BRANCH_NAME"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Commit translation changes
|
||||||
|
if: steps.pr_meta.outputs.has_changes == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
git -C "${{ github.workspace }}" checkout -B "${{ steps.pr_meta.outputs.branch_name }}"
|
||||||
|
git -C "${{ github.workspace }}" add web/i18n/
|
||||||
|
git -C "${{ github.workspace }}" commit -m "chore(i18n): sync translations with en-US"
|
||||||
|
|
||||||
|
- name: Push translation branch
|
||||||
|
if: steps.pr_meta.outputs.has_changes == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if git -C "${{ github.workspace }}" ls-remote --exit-code --heads origin "${{ steps.pr_meta.outputs.branch_name }}" >/dev/null 2>&1; then
|
||||||
|
git -C "${{ github.workspace }}" push --force-with-lease origin "${{ steps.pr_meta.outputs.branch_name }}"
|
||||||
|
else
|
||||||
|
git -C "${{ github.workspace }}" push --set-upstream origin "${{ steps.pr_meta.outputs.branch_name }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create or update translation PR
|
||||||
|
if: steps.pr_meta.outputs.has_changes == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BRANCH_NAME: ${{ steps.pr_meta.outputs.branch_name }}
|
||||||
|
FILES_IN_SCOPE: ${{ steps.context.outputs.CHANGED_FILES }}
|
||||||
|
TARGET_LANGS: ${{ steps.context.outputs.TARGET_LANGS }}
|
||||||
|
SYNC_MODE: ${{ steps.context.outputs.SYNC_MODE }}
|
||||||
|
CHANGES_SOURCE: ${{ steps.context.outputs.CHANGES_SOURCE }}
|
||||||
|
BASE_SHA: ${{ steps.context.outputs.BASE_SHA }}
|
||||||
|
HEAD_SHA: ${{ steps.context.outputs.HEAD_SHA }}
|
||||||
|
REPO_NAME: ${{ github.repository }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
PR_BODY_FILE=/tmp/i18n-pr-body.md
|
||||||
|
LANG_COUNT=$(printf '%s\n' "$TARGET_LANGS" | wc -w | tr -d ' ')
|
||||||
|
if [ "$LANG_COUNT" = "0" ]; then
|
||||||
|
LANG_COUNT="0"
|
||||||
|
fi
|
||||||
|
export LANG_COUNT
|
||||||
|
|
||||||
|
node <<'NODE' > "$PR_BODY_FILE"
|
||||||
|
const fs = require('node:fs')
|
||||||
|
|
||||||
|
const changesPath = '/tmp/i18n-changes.json'
|
||||||
|
const changes = fs.existsSync(changesPath)
|
||||||
|
? JSON.parse(fs.readFileSync(changesPath, 'utf8'))
|
||||||
|
: { changes: {} }
|
||||||
|
|
||||||
|
const filesInScope = (process.env.FILES_IN_SCOPE || '').split(/\s+/).filter(Boolean)
|
||||||
|
const lines = [
|
||||||
|
'## Summary',
|
||||||
|
'',
|
||||||
|
`- **Files synced**: \`${process.env.FILES_IN_SCOPE || '<none>'}\``,
|
||||||
|
`- **Languages updated**: ${process.env.TARGET_LANGS || '<none>'} (${process.env.LANG_COUNT} languages)`,
|
||||||
|
`- **Sync mode**: ${process.env.SYNC_MODE}${process.env.BASE_SHA ? ` (base: \`${process.env.BASE_SHA.slice(0, 10)}\`, head: \`${process.env.HEAD_SHA.slice(0, 10)}\`)` : ` (head: \`${process.env.HEAD_SHA.slice(0, 10)}\`)`}`,
|
||||||
|
'',
|
||||||
|
'### Key changes',
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const fileName of filesInScope) {
|
||||||
|
const fileChange = changes.changes?.[fileName] || { added: {}, updated: {}, deleted: [], fileDeleted: false }
|
||||||
|
const addedKeys = Object.keys(fileChange.added || {})
|
||||||
|
const updatedKeys = Object.keys(fileChange.updated || {})
|
||||||
|
const deletedKeys = fileChange.deleted || []
|
||||||
|
lines.push(`- \`${fileName}\`: +${addedKeys.length} / ~${updatedKeys.length} / -${deletedKeys.length}${fileChange.fileDeleted ? ' (file deleted in en-US)' : ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
'',
|
||||||
|
'## Verification',
|
||||||
|
'',
|
||||||
|
`- \`vp run dify-web#i18n:check --file ${process.env.FILES_IN_SCOPE} --lang ${process.env.TARGET_LANGS}\``,
|
||||||
|
`- \`vp run dify-web#lint:fix --quiet -- <edited i18n files under web/>\``,
|
||||||
|
'',
|
||||||
|
'## Notes',
|
||||||
|
'',
|
||||||
|
'- This PR was generated from structured en-US key changes produced by `trigger-i18n-sync.yml`.',
|
||||||
|
`- Structured change source: ${process.env.CHANGES_SOURCE || 'unknown'}.`,
|
||||||
|
'- Branch name is deterministic for the head SHA and scope, so reruns update the same PR instead of opening duplicates.',
|
||||||
|
'',
|
||||||
|
'🤖 Generated with [Claude Code](https://claude.com/claude-code)'
|
||||||
|
)
|
||||||
|
|
||||||
|
process.stdout.write(lines.join('\n'))
|
||||||
|
NODE
|
||||||
|
|
||||||
|
EXISTING_PR_NUMBER=$(gh pr list --repo "$REPO_NAME" --head "$BRANCH_NAME" --state open --json number --jq '.[0].number')
|
||||||
|
|
||||||
|
if [ -n "$EXISTING_PR_NUMBER" ] && [ "$EXISTING_PR_NUMBER" != "null" ]; then
|
||||||
|
gh pr edit "$EXISTING_PR_NUMBER" --repo "$REPO_NAME" --title "chore(i18n): sync translations with en-US" --body-file "$PR_BODY_FILE"
|
||||||
|
else
|
||||||
|
gh pr create --repo "$REPO_NAME" --head "$BRANCH_NAME" --base main --title "chore(i18n): sync translations with en-US" --body-file "$PR_BODY_FILE"
|
||||||
|
fi
|
||||||
|
|||||||
92
.github/workflows/trigger-i18n-sync.yml
vendored
92
.github/workflows/trigger-i18n-sync.yml
vendored
@ -1,9 +1,5 @@
|
|||||||
name: Trigger i18n Sync on Push
|
name: Trigger i18n Sync on Push
|
||||||
|
|
||||||
# This workflow bridges the push event to repository_dispatch
|
|
||||||
# because claude-code-action doesn't support push events directly.
|
|
||||||
# See: https://github.com/langgenius/dify/issues/30743
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
@ -13,10 +9,14 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: trigger-i18n-sync-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
trigger:
|
trigger:
|
||||||
if: github.repository == 'langgenius/dify'
|
if: github.repository == 'langgenius/dify'
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -25,42 +25,66 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Detect changed files and generate diff
|
- name: Detect changed files and build structured change set
|
||||||
id: detect
|
id: detect
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
BEFORE_SHA="${{ github.event.before }}"
|
BASE_SHA="${{ github.event.before }}"
|
||||||
# Handle edge case: force push may have null/zero SHA
|
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||||
if [ -z "$BEFORE_SHA" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || true)
|
||||||
BEFORE_SHA="HEAD~1"
|
|
||||||
fi
|
fi
|
||||||
|
HEAD_SHA="${{ github.sha }}"
|
||||||
|
|
||||||
# Detect changed i18n files
|
if [ -n "$BASE_SHA" ]; then
|
||||||
changed=$(git diff --name-only "$BEFORE_SHA" "${{ github.sha }}" -- 'web/i18n/en-US/*.json' 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/.json$//' | tr '\n' ' ' || echo "")
|
CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' 2>/dev/null | sed -n 's@^.*/@@p' | sed 's/\.json$//' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||||
echo "changed_files=$changed" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Generate diff for context
|
|
||||||
git diff "$BEFORE_SHA" "${{ github.sha }}" -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || echo "" > /tmp/i18n-diff.txt
|
|
||||||
|
|
||||||
# Truncate if too large (keep first 50KB to match receiving workflow)
|
|
||||||
head -c 50000 /tmp/i18n-diff.txt > /tmp/i18n-diff-truncated.txt
|
|
||||||
mv /tmp/i18n-diff-truncated.txt /tmp/i18n-diff.txt
|
|
||||||
|
|
||||||
# Base64 encode the diff for safe JSON transport (portable, single-line)
|
|
||||||
diff_base64=$(base64 < /tmp/i18n-diff.txt | tr -d '\n')
|
|
||||||
echo "diff_base64=$diff_base64" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
if [ -n "$changed" ]; then
|
|
||||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "Detected changed files: $changed"
|
|
||||||
else
|
else
|
||||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
CHANGED_FILES=$(find web/i18n/en-US -maxdepth 1 -type f -name '*.json' -print | sed -n 's@^.*/@@p' | sed 's/\.json$//' | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||||
echo "No i18n changes detected"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
export BASE_SHA HEAD_SHA CHANGED_FILES
|
||||||
|
node .github/scripts/generate-i18n-changes.mjs
|
||||||
|
|
||||||
|
if [ -n "$CHANGED_FILES" ]; then
|
||||||
|
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "base_sha=$BASE_SHA" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "changed_files=$CHANGED_FILES" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Trigger i18n sync workflow
|
- name: Trigger i18n sync workflow
|
||||||
if: steps.detect.outputs.has_changes == 'true'
|
if: steps.detect.outputs.has_changes == 'true'
|
||||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
env:
|
||||||
|
BASE_SHA: ${{ steps.detect.outputs.base_sha }}
|
||||||
|
HEAD_SHA: ${{ steps.detect.outputs.head_sha }}
|
||||||
|
CHANGED_FILES: ${{ steps.detect.outputs.changed_files }}
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
event-type: i18n-sync
|
script: |
|
||||||
client-payload: '{"changed_files": "${{ steps.detect.outputs.changed_files }}", "diff_base64": "${{ steps.detect.outputs.diff_base64 }}", "sync_mode": "incremental", "trigger_sha": "${{ github.sha }}"}'
|
const fs = require('fs')
|
||||||
|
|
||||||
|
const changesJson = fs.readFileSync('/tmp/i18n-changes.json', 'utf8')
|
||||||
|
const changesBase64 = Buffer.from(changesJson).toString('base64')
|
||||||
|
const maxEmbeddedChangesChars = 48000
|
||||||
|
const changesEmbedded = changesBase64.length <= maxEmbeddedChangesChars
|
||||||
|
|
||||||
|
if (!changesEmbedded) {
|
||||||
|
console.log(`Structured change set too large to embed safely (${changesBase64.length} chars). Downstream workflow will regenerate it from git history.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.repos.createDispatchEvent({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
event_type: 'i18n-sync',
|
||||||
|
client_payload: {
|
||||||
|
changed_files: process.env.CHANGED_FILES,
|
||||||
|
changes_base64: changesEmbedded ? changesBase64 : '',
|
||||||
|
changes_embedded: changesEmbedded,
|
||||||
|
sync_mode: 'incremental',
|
||||||
|
base_sha: process.env.BASE_SHA,
|
||||||
|
head_sha: process.env.HEAD_SHA,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
68
.github/workflows/vdb-tests-full.yml
vendored
Normal file
68
.github/workflows/vdb-tests-full.yml
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
name: Run Full VDB Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 3 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: vdb-tests-full-${{ github.ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Full VDB Tests
|
||||||
|
if: github.repository == 'langgenius/dify'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version:
|
||||||
|
- "3.12"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Free Disk Space
|
||||||
|
uses: endersonmenezes/free-disk-space@7901478139cff6e9d44df5972fd8ab8fcade4db1 # v3.2.2
|
||||||
|
with:
|
||||||
|
remove_dotnet: true
|
||||||
|
remove_haskell: true
|
||||||
|
remove_tool_cache: true
|
||||||
|
|
||||||
|
- name: Setup UV and Python
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache-dependency-glob: api/uv.lock
|
||||||
|
|
||||||
|
- name: Check UV lockfile
|
||||||
|
run: uv lock --project api --check
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
|
# - name: Set up Vector Store (TiDB)
|
||||||
|
# uses: hoverkraft-tech/compose-action@v2.0.2
|
||||||
|
# with:
|
||||||
|
# compose-file: docker/tidb/docker-compose.yaml
|
||||||
|
# services: |
|
||||||
|
# tidb
|
||||||
|
# tiflash
|
||||||
|
|
||||||
|
# - name: Check VDB Ready (TiDB)
|
||||||
|
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
|
||||||
|
|
||||||
|
- name: Test Vector Stores
|
||||||
|
run: |
|
||||||
|
uv run --project api pytest \
|
||||||
|
--start-vdb \
|
||||||
|
--vdb-services "weaviate,qdrant,couchbase-server,etcd,minio,milvus-standalone,pgvecto-rs,pgvector,chroma,elasticsearch,oceanbase" \
|
||||||
|
--timeout "${PYTEST_TIMEOUT:-180}" \
|
||||||
|
api/providers/vdb/*/tests/integration_tests
|
||||||
55
.github/workflows/vdb-tests.yml
vendored
55
.github/workflows/vdb-tests.yml
vendored
@ -1,20 +1,22 @@
|
|||||||
name: Run VDB Tests
|
name: Run VDB Smoke Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: vdb-tests-${{ github.head_ref || github.run_id }}
|
group: vdb-tests-${{ github.head_ref || github.run_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: VDB Tests
|
name: VDB Smoke Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- "3.11"
|
|
||||||
- "3.12"
|
- "3.12"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -31,7 +33,7 @@ jobs:
|
|||||||
remove_tool_cache: true
|
remove_tool_cache: true
|
||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@ -43,14 +45,6 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: uv sync --project api --dev
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
- name: Set up dotenvs
|
|
||||||
run: |
|
|
||||||
cp docker/.env.example docker/.env
|
|
||||||
cp docker/middleware.env.example docker/middleware.env
|
|
||||||
|
|
||||||
- name: Expose Service Ports
|
|
||||||
run: sh .github/workflows/expose_service_ports.sh
|
|
||||||
|
|
||||||
# - name: Set up Vector Store (TiDB)
|
# - name: Set up Vector Store (TiDB)
|
||||||
# uses: hoverkraft-tech/compose-action@v2.0.2
|
# uses: hoverkraft-tech/compose-action@v2.0.2
|
||||||
# with:
|
# with:
|
||||||
@ -59,32 +53,15 @@ jobs:
|
|||||||
# tidb
|
# tidb
|
||||||
# tiflash
|
# tiflash
|
||||||
|
|
||||||
- name: Set up Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase, OceanBase)
|
|
||||||
uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
|
|
||||||
with:
|
|
||||||
compose-file: |
|
|
||||||
docker/docker-compose.yaml
|
|
||||||
services: |
|
|
||||||
weaviate
|
|
||||||
qdrant
|
|
||||||
couchbase-server
|
|
||||||
etcd
|
|
||||||
minio
|
|
||||||
milvus-standalone
|
|
||||||
pgvecto-rs
|
|
||||||
pgvector
|
|
||||||
chroma
|
|
||||||
elasticsearch
|
|
||||||
oceanbase
|
|
||||||
|
|
||||||
- name: setup test config
|
|
||||||
run: |
|
|
||||||
echo $(pwd)
|
|
||||||
ls -lah .
|
|
||||||
cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env
|
|
||||||
|
|
||||||
# - name: Check VDB Ready (TiDB)
|
# - name: Check VDB Ready (TiDB)
|
||||||
# run: uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py
|
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
|
||||||
|
|
||||||
- name: Test Vector Stores
|
- name: Test Vector Stores
|
||||||
run: uv run --project api bash dev/pytest/pytest_vdb.sh
|
run: |
|
||||||
|
uv run --project api pytest \
|
||||||
|
--start-vdb \
|
||||||
|
--timeout "${PYTEST_TIMEOUT:-180}" \
|
||||||
|
api/providers/vdb/vdb-chroma/tests/integration_tests \
|
||||||
|
api/providers/vdb/vdb-pgvector/tests/integration_tests \
|
||||||
|
api/providers/vdb/vdb-qdrant/tests/integration_tests \
|
||||||
|
api/providers/vdb/vdb-weaviate/tests/integration_tests
|
||||||
|
|||||||
68
.github/workflows/web-e2e.yml
vendored
Normal file
68
.github/workflows/web-e2e.yml
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
name: Web Full-Stack E2E
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: web-e2e-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Web Full-Stack E2E
|
||||||
|
runs-on: depot-ubuntu-24.04-4
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup web dependencies
|
||||||
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
|
- name: Setup UV and Python
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
python-version: "3.12"
|
||||||
|
cache-dependency-glob: api/uv.lock
|
||||||
|
|
||||||
|
- name: Install API dependencies
|
||||||
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
|
- name: Install Playwright browser
|
||||||
|
working-directory: ./e2e
|
||||||
|
run: vp run e2e:install
|
||||||
|
|
||||||
|
- name: Run isolated source-api and built-web Cucumber E2E tests
|
||||||
|
working-directory: ./e2e
|
||||||
|
env:
|
||||||
|
E2E_ADMIN_EMAIL: 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
|
||||||
54
.github/workflows/web-tests.yml
vendored
54
.github/workflows/web-tests.yml
vendored
@ -16,14 +16,14 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Web Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
name: Web Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04-4
|
||||||
env:
|
env:
|
||||||
VITEST_COVERAGE_SCOPE: app-components
|
VITEST_COVERAGE_SCOPE: app-components
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
shardIndex: [1, 2, 3, 4, 5, 6]
|
shardIndex: [1, 2, 3, 4]
|
||||||
shardTotal: [6]
|
shardTotal: [4]
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -39,11 +39,11 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-web
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: vp test run --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --coverage
|
run: vp test run --reporter=blob --reporter=minimal --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --coverage
|
||||||
|
|
||||||
- name: Upload blob report
|
- name: Upload blob report
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: blob-report-${{ matrix.shardIndex }}
|
name: blob-report-${{ matrix.shardIndex }}
|
||||||
path: web/.vitest-reports/*
|
path: web/.vitest-reports/*
|
||||||
@ -54,7 +54,7 @@ jobs:
|
|||||||
name: Merge Test Reports
|
name: Merge Test Reports
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
needs: [test]
|
needs: [test]
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04-4
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
defaults:
|
defaults:
|
||||||
@ -66,7 +66,6 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup web environment
|
- name: Setup web environment
|
||||||
@ -84,19 +83,22 @@ jobs:
|
|||||||
|
|
||||||
- name: Report coverage
|
- name: Report coverage
|
||||||
if: ${{ env.CODECOV_TOKEN != '' }}
|
if: ${{ env.CODECOV_TOKEN != '' }}
|
||||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||||
with:
|
with:
|
||||||
directory: web/coverage
|
directory: web/coverage
|
||||||
flags: web
|
flags: web
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}
|
||||||
|
|
||||||
web-build:
|
dify-ui-test:
|
||||||
name: Web Build
|
name: dify-ui Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04-4
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./web
|
shell: bash
|
||||||
|
working-directory: ./packages/dify-ui
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@ -104,20 +106,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check changed files
|
|
||||||
id: changed-files
|
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
web/**
|
|
||||||
.github/workflows/web-tests.yml
|
|
||||||
.github/actions/setup-web/**
|
|
||||||
|
|
||||||
- name: Setup web environment
|
- name: Setup web environment
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
uses: ./.github/actions/setup-web
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
- name: Web build check
|
- name: Install Chromium for Browser Mode
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
run: vp exec playwright install --with-deps chromium
|
||||||
working-directory: ./web
|
|
||||||
run: vp run build
|
- name: Run dify-ui tests
|
||||||
|
run: vp test run --coverage --silent=passed-only
|
||||||
|
|
||||||
|
- name: Report coverage
|
||||||
|
if: ${{ env.CODECOV_TOKEN != '' }}
|
||||||
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||||
|
with:
|
||||||
|
directory: packages/dify-ui/coverage
|
||||||
|
flags: dify-ui
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}
|
||||||
|
|||||||
14
.gitignore
vendored
14
.gitignore
vendored
@ -203,26 +203,30 @@ sdks/python-client/dify_client.egg-info
|
|||||||
|
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/launch.json.template
|
!.vscode/launch.json.template
|
||||||
|
!.vscode/settings.example.json
|
||||||
!.vscode/README.md
|
!.vscode/README.md
|
||||||
api/.vscode
|
api/.vscode
|
||||||
# vscode Code History Extension
|
# vscode Code History Extension
|
||||||
.history
|
.history
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
web/migration/
|
|
||||||
|
|
||||||
# pnpm
|
# pnpm
|
||||||
/.pnpm-store
|
/.pnpm-store
|
||||||
|
node_modules
|
||||||
|
.vite-hooks/_
|
||||||
|
|
||||||
# plugin migrate
|
# plugin migrate
|
||||||
plugins.jsonl
|
plugins.jsonl
|
||||||
|
|
||||||
|
# generated API OpenAPI specs
|
||||||
|
packages/contracts/openapi/
|
||||||
|
|
||||||
# mise
|
# mise
|
||||||
mise.toml
|
mise.toml
|
||||||
|
|
||||||
|
|
||||||
# AI Assistant
|
# AI Assistant
|
||||||
.sisyphus/
|
|
||||||
.roo/
|
.roo/
|
||||||
/.claude/worktrees/
|
/.claude/worktrees/
|
||||||
api/.env.backup
|
api/.env.backup
|
||||||
@ -236,9 +240,15 @@ 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
|
# Code Agent Folder
|
||||||
.qoder/*
|
.qoder/*
|
||||||
|
.context/*
|
||||||
|
.eslintcache
|
||||||
|
|||||||
64
.vite-hooks/pre-commit
Executable file
64
.vite-hooks/pre-commit
Executable file
@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# get the list of modified files
|
||||||
|
files=$(git diff --cached --name-only)
|
||||||
|
|
||||||
|
# check if api or web directory is modified
|
||||||
|
|
||||||
|
api_modified=false
|
||||||
|
web_modified=false
|
||||||
|
skip_web_checks=false
|
||||||
|
|
||||||
|
git_path() {
|
||||||
|
git rev-parse --git-path "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -f "$(git_path MERGE_HEAD)" ] || \
|
||||||
|
[ -f "$(git_path CHERRY_PICK_HEAD)" ] || \
|
||||||
|
[ -f "$(git_path REVERT_HEAD)" ] || \
|
||||||
|
[ -f "$(git_path SQUASH_MSG)" ] || \
|
||||||
|
[ -d "$(git_path rebase-merge)" ] || \
|
||||||
|
[ -d "$(git_path rebase-apply)" ]; then
|
||||||
|
skip_web_checks=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
for file in $files
|
||||||
|
do
|
||||||
|
# Use POSIX compliant pattern matching
|
||||||
|
case "$file" in
|
||||||
|
api/*.py)
|
||||||
|
# set api_modified flag to true
|
||||||
|
api_modified=true
|
||||||
|
;;
|
||||||
|
web/*)
|
||||||
|
# set web_modified flag to true
|
||||||
|
web_modified=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# run linters based on the modified modules
|
||||||
|
|
||||||
|
if $api_modified; then
|
||||||
|
echo "Running Ruff linter on api module"
|
||||||
|
|
||||||
|
# run Ruff linter auto-fixing
|
||||||
|
uv run --project api --dev ruff check --fix ./api
|
||||||
|
|
||||||
|
# run Ruff linter checks
|
||||||
|
uv run --project api --dev ruff check ./api || status=$?
|
||||||
|
|
||||||
|
status=${status:-0}
|
||||||
|
|
||||||
|
if [ $status -ne 0 ]; then
|
||||||
|
echo "Ruff linter on api module error, exit code: $status"
|
||||||
|
echo "Please run 'dev/reformat' to fix the fixable linting errors."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $skip_web_checks; then
|
||||||
|
echo "Git operation in progress, skipping web checks"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
vp staged
|
||||||
15
.vscode/launch.json.template
vendored
15
.vscode/launch.json.template
vendored
@ -2,21 +2,10 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Python: Flask API",
|
"name": "Python: API (gevent)",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "flask",
|
"program": "${workspaceFolder}/api/app.py",
|
||||||
"env": {
|
|
||||||
"FLASK_APP": "app.py",
|
|
||||||
"FLASK_ENV": "development"
|
|
||||||
},
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"--host=0.0.0.0",
|
|
||||||
"--port=5001",
|
|
||||||
"--no-debugger",
|
|
||||||
"--no-reload"
|
|
||||||
],
|
|
||||||
"jinja": true,
|
"jinja": true,
|
||||||
"justMyCode": true,
|
"justMyCode": true,
|
||||||
"cwd": "${workspaceFolder}/api",
|
"cwd": "${workspaceFolder}/api",
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
{
|
{
|
||||||
// Disable the default formatter, use eslint instead
|
"cucumber.features": [
|
||||||
"prettier.enable": false,
|
"e2e/features/**/*.feature",
|
||||||
"editor.formatOnSave": false,
|
],
|
||||||
|
"cucumber.glue": [
|
||||||
|
"e2e/features/**/*.ts",
|
||||||
|
],
|
||||||
|
|
||||||
|
"tailwindCSS.experimental.configFile": "web/app/styles/globals.css",
|
||||||
|
|
||||||
// Auto fix
|
// Auto fix
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit",
|
||||||
"source.organizeImports": "never"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Silent the stylistic rules in your IDE, but still auto fix them
|
// Silent the stylistic rules in your IDE, but still auto fix them
|
||||||
@ -9,6 +9,7 @@ The codebase is split into:
|
|||||||
- **Backend API** (`/api`): Python Flask application organized with Domain-Driven Design
|
- **Backend API** (`/api`): Python Flask application organized with Domain-Driven Design
|
||||||
- **Frontend Web** (`/web`): Next.js application using TypeScript and React
|
- **Frontend Web** (`/web`): Next.js application using TypeScript and React
|
||||||
- **Docker deployment** (`/docker`): Containerized deployment configurations
|
- **Docker deployment** (`/docker`): Containerized deployment configurations
|
||||||
|
- **Dify Agent Backend** (`/dify-agent`): Backend services for managing and executing agent
|
||||||
|
|
||||||
## Backend Workflow
|
## Backend Workflow
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ The codebase is split into:
|
|||||||
## Language Style
|
## Language Style
|
||||||
|
|
||||||
- **Python**: Keep type hints on functions and attributes, and implement relevant special methods (e.g., `__repr__`, `__str__`). Prefer `TypedDict` over `dict` or `Mapping` for type safety and better code documentation.
|
- **Python**: Keep type hints on functions and attributes, and implement relevant special methods (e.g., `__repr__`, `__str__`). Prefer `TypedDict` over `dict` or `Mapping` for type safety and better code documentation.
|
||||||
- **TypeScript**: Use the strict config, rely on ESLint (`pnpm lint:fix` preferred) plus `pnpm type-check:tsgo`, and avoid `any` types.
|
- **TypeScript**: Use the strict config, rely on ESLint (`pnpm lint:fix` preferred) plus `pnpm type-check`, and avoid `any` types.
|
||||||
|
|
||||||
## General Practices
|
## General Practices
|
||||||
|
|
||||||
|
|||||||
@ -97,8 +97,3 @@ Feel free to reach out if you encounter any issues during the setup process.
|
|||||||
## Getting Help
|
## Getting Help
|
||||||
|
|
||||||
If you ever get stuck or get a burning question while contributing, simply shoot your queries our way via the related GitHub issue, or hop onto our [Discord](https://discord.gg/8Tpq4AcN9c) for a quick chat.
|
If you ever get stuck or get a burning question while contributing, simply shoot your queries our way via the related GitHub issue, or hop onto our [Discord](https://discord.gg/8Tpq4AcN9c) for a quick chat.
|
||||||
|
|
||||||
## Automated Agent Contributions
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> If you are an automated agent, just add 🤖🤖🤖 to the end of the PR title to opt-in.
|
|
||||||
|
|||||||
98
Makefile
98
Makefile
@ -3,6 +3,10 @@ DOCKER_REGISTRY=langgenius
|
|||||||
WEB_IMAGE=$(DOCKER_REGISTRY)/dify-web
|
WEB_IMAGE=$(DOCKER_REGISTRY)/dify-web
|
||||||
API_IMAGE=$(DOCKER_REGISTRY)/dify-api
|
API_IMAGE=$(DOCKER_REGISTRY)/dify-api
|
||||||
VERSION=latest
|
VERSION=latest
|
||||||
|
DOCKER_DIR=docker
|
||||||
|
DOCKER_MIDDLEWARE_ENV=$(DOCKER_DIR)/middleware.env
|
||||||
|
DOCKER_MIDDLEWARE_ENV_EXAMPLE=$(DOCKER_DIR)/envs/middleware.env.example
|
||||||
|
DOCKER_MIDDLEWARE_PROJECT=dify-middlewares-dev
|
||||||
|
|
||||||
# Default target - show help
|
# Default target - show help
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
@ -17,15 +21,20 @@ dev-setup: prepare-docker prepare-web prepare-api
|
|||||||
# Step 1: Prepare Docker middleware
|
# Step 1: Prepare Docker middleware
|
||||||
prepare-docker:
|
prepare-docker:
|
||||||
@echo "🐳 Setting up Docker middleware..."
|
@echo "🐳 Setting up Docker middleware..."
|
||||||
@cp -n docker/middleware.env.example docker/middleware.env 2>/dev/null || echo "Docker middleware.env already exists"
|
@if [ ! -f "$(DOCKER_MIDDLEWARE_ENV)" ]; then \
|
||||||
@cd docker && docker compose -f docker-compose.middleware.yaml --env-file middleware.env -p dify-middlewares-dev up -d
|
cp "$(DOCKER_MIDDLEWARE_ENV_EXAMPLE)" "$(DOCKER_MIDDLEWARE_ENV)"; \
|
||||||
|
echo "Docker middleware.env created"; \
|
||||||
|
else \
|
||||||
|
echo "Docker middleware.env already exists"; \
|
||||||
|
fi
|
||||||
|
@cd $(DOCKER_DIR) && docker compose -f docker-compose.middleware.yaml --env-file middleware.env -p $(DOCKER_MIDDLEWARE_PROJECT) up -d
|
||||||
@echo "✅ Docker middleware started"
|
@echo "✅ Docker middleware started"
|
||||||
|
|
||||||
# Step 2: Prepare web environment
|
# Step 2: Prepare web environment
|
||||||
prepare-web:
|
prepare-web:
|
||||||
@echo "🌐 Setting up web environment..."
|
@echo "🌐 Setting up web environment..."
|
||||||
@cp -n web/.env.example web/.env 2>/dev/null || echo "Web .env already exists"
|
@cp -n web/.env.example web/.env.local 2>/dev/null || echo "Web .env.local already exists"
|
||||||
@cd web && pnpm install
|
@pnpm install
|
||||||
@echo "✅ Web environment prepared (not started)"
|
@echo "✅ Web environment prepared (not started)"
|
||||||
|
|
||||||
# Step 3: Prepare API environment
|
# Step 3: Prepare API environment
|
||||||
@ -39,12 +48,18 @@ prepare-api:
|
|||||||
# Clean dev environment
|
# Clean dev environment
|
||||||
dev-clean:
|
dev-clean:
|
||||||
@echo "⚠️ Stopping Docker containers..."
|
@echo "⚠️ Stopping Docker containers..."
|
||||||
@cd docker && docker compose -f docker-compose.middleware.yaml --env-file middleware.env -p dify-middlewares-dev down
|
@if [ -f "$(DOCKER_MIDDLEWARE_ENV)" ]; then \
|
||||||
|
cd $(DOCKER_DIR) && docker compose -f docker-compose.middleware.yaml --env-file middleware.env -p $(DOCKER_MIDDLEWARE_PROJECT) down; \
|
||||||
|
else \
|
||||||
|
echo "Docker middleware.env does not exist, skipping compose down"; \
|
||||||
|
fi
|
||||||
@echo "🗑️ Removing volumes..."
|
@echo "🗑️ Removing volumes..."
|
||||||
@rm -rf docker/volumes/db
|
@rm -rf docker/volumes/db
|
||||||
|
@rm -rf docker/volumes/mysql
|
||||||
@rm -rf docker/volumes/redis
|
@rm -rf docker/volumes/redis
|
||||||
@rm -rf docker/volumes/plugin_daemon
|
@rm -rf docker/volumes/plugin_daemon
|
||||||
@rm -rf docker/volumes/weaviate
|
@rm -rf docker/volumes/weaviate
|
||||||
|
@rm -rf docker/volumes/sandbox/dependencies
|
||||||
@rm -rf api/storage
|
@rm -rf api/storage
|
||||||
@echo "✅ Cleanup complete"
|
@echo "✅ Cleanup complete"
|
||||||
|
|
||||||
@ -60,34 +75,84 @@ check:
|
|||||||
@echo "✅ Code check complete"
|
@echo "✅ Code check complete"
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@echo "🔧 Running ruff format, check with fixes, import linter, and dotenv-linter..."
|
@echo "🔧 Running ruff format, check with fixes, response contract lint, import linter, and dotenv-linter..."
|
||||||
@uv run --project api --dev ruff format ./api
|
@uv run --project api --dev ruff format ./api
|
||||||
@uv run --project api --dev ruff check --fix ./api
|
@uv run --project api --dev ruff check --fix ./api
|
||||||
|
@$(MAKE) api-contract-lint
|
||||||
@uv run --directory api --dev lint-imports
|
@uv run --directory api --dev lint-imports
|
||||||
@uv run --project api --dev dotenv-linter ./api/.env.example ./web/.env.example
|
@uv run --project api --dev dotenv-linter ./api/.env.example ./web/.env.example
|
||||||
@echo "✅ Linting complete"
|
@echo "✅ Linting complete"
|
||||||
|
|
||||||
|
api-contract-lint:
|
||||||
|
@echo "🔎 Linting Flask response contracts..."
|
||||||
|
@uv run --project api --dev python api/dev/lint_response_contracts.py
|
||||||
|
@echo "✅ Response contract lint complete"
|
||||||
|
|
||||||
type-check:
|
type-check:
|
||||||
@echo "📝 Running type checks (basedpyright + pyrefly + mypy)..."
|
@echo "📝 Running type checks (pyrefly + mypy)..."
|
||||||
@./dev/basedpyright-check $(PATH_TO_CHECK)
|
@./dev/pyrefly-check-local $(PATH_TO_CHECK)
|
||||||
@./dev/pyrefly-check-local
|
@uv --directory api run mypy --exclude-gitignore --exclude '(^|/)conftest\.py$$' --exclude 'tests/' --exclude 'migrations/' --exclude 'dev/generate_swagger_specs.py' --exclude 'dev/generate_fastopenapi_specs.py' --check-untyped-defs --disable-error-code=import-untyped .
|
||||||
@uv --directory api run mypy --exclude-gitignore --exclude 'tests/' --exclude 'migrations/' --check-untyped-defs --disable-error-code=import-untyped .
|
|
||||||
@echo "✅ Type checks complete"
|
@echo "✅ Type checks complete"
|
||||||
|
|
||||||
|
type-check-core:
|
||||||
|
@echo "📝 Running core type checks (pyrefly + mypy)..."
|
||||||
|
@./dev/pyrefly-check-local $(PATH_TO_CHECK)
|
||||||
|
@uv --directory api run mypy --exclude-gitignore --exclude '(^|/)conftest\.py$$' --exclude 'tests/' --exclude 'migrations/' --exclude 'dev/generate_swagger_specs.py' --exclude 'dev/generate_fastopenapi_specs.py' --check-untyped-defs --disable-error-code=import-untyped .
|
||||||
|
@echo "✅ Core type checks complete"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@echo "🧪 Running backend unit tests..."
|
@echo "🧪 Running backend unit tests..."
|
||||||
@if [ -n "$(TARGET_TESTS)" ]; then \
|
@if [ -n "$(TARGET_TESTS)" ]; then \
|
||||||
echo "Target: $(TARGET_TESTS)"; \
|
echo "Target: $(TARGET_TESTS)"; \
|
||||||
uv run --project api --dev pytest $(TARGET_TESTS); \
|
uv run --project api --dev pytest $(TARGET_TESTS); \
|
||||||
else \
|
else \
|
||||||
PYTEST_XDIST_ARGS="-n auto" uv run --project api --dev dev/pytest/pytest_unit_tests.sh; \
|
echo "Running backend unit tests"; \
|
||||||
|
uv run --project api --dev pytest -p no:benchmark --timeout "$${PYTEST_TIMEOUT:-20}" -n auto \
|
||||||
|
api/tests/unit_tests \
|
||||||
|
api/providers/vdb/*/tests/unit_tests \
|
||||||
|
api/providers/trace/*/tests/unit_tests \
|
||||||
|
--ignore=api/tests/unit_tests/controllers; \
|
||||||
|
uv run --project api --dev pytest --timeout "$${PYTEST_TIMEOUT:-20}" --cov-append \
|
||||||
|
api/tests/unit_tests/controllers; \
|
||||||
|
fi
|
||||||
|
@echo "✅ Unit tests complete"
|
||||||
|
|
||||||
|
test-all:
|
||||||
|
@echo "🧪 Running full backend test suite..."
|
||||||
|
@if [ -n "$(TARGET_TESTS)" ]; then \
|
||||||
|
echo "Target: $(TARGET_TESTS)"; \
|
||||||
|
uv run --project api --dev pytest $(TARGET_TESTS); \
|
||||||
|
else \
|
||||||
|
echo "Running backend unit tests"; \
|
||||||
|
uv run --project api --dev pytest -p no:benchmark --timeout "$${PYTEST_TIMEOUT:-20}" -n auto \
|
||||||
|
api/tests/unit_tests \
|
||||||
|
api/providers/vdb/*/tests/unit_tests \
|
||||||
|
api/providers/trace/*/tests/unit_tests \
|
||||||
|
--ignore=api/tests/unit_tests/controllers; \
|
||||||
|
uv run --project api --dev pytest --timeout "$${PYTEST_TIMEOUT:-20}" --cov-append \
|
||||||
|
api/tests/unit_tests/controllers; \
|
||||||
|
echo "Running backend integration tests"; \
|
||||||
|
uv run --project api --dev pytest -p no:benchmark --start-middleware -n auto \
|
||||||
|
--timeout "$${PYTEST_TIMEOUT:-180}" \
|
||||||
|
--cov-append \
|
||||||
|
api/tests/integration_tests/workflow \
|
||||||
|
api/tests/integration_tests/tools \
|
||||||
|
api/tests/test_containers_integration_tests; \
|
||||||
|
echo "Running VDB smoke tests"; \
|
||||||
|
uv run --project api --dev pytest --start-vdb \
|
||||||
|
--timeout "$${PYTEST_TIMEOUT:-180}" \
|
||||||
|
--cov-append \
|
||||||
|
api/providers/vdb/vdb-chroma/tests/integration_tests \
|
||||||
|
api/providers/vdb/vdb-pgvector/tests/integration_tests \
|
||||||
|
api/providers/vdb/vdb-qdrant/tests/integration_tests \
|
||||||
|
api/providers/vdb/vdb-weaviate/tests/integration_tests; \
|
||||||
fi
|
fi
|
||||||
@echo "✅ Tests complete"
|
@echo "✅ Tests complete"
|
||||||
|
|
||||||
# Build Docker images
|
# Build Docker images
|
||||||
build-web:
|
build-web:
|
||||||
@echo "Building web Docker image: $(WEB_IMAGE):$(VERSION)..."
|
@echo "Building web Docker image: $(WEB_IMAGE):$(VERSION)..."
|
||||||
docker build -t $(WEB_IMAGE):$(VERSION) ./web
|
docker build -f web/Dockerfile -t $(WEB_IMAGE):$(VERSION) .
|
||||||
@echo "Web Docker image built successfully: $(WEB_IMAGE):$(VERSION)"
|
@echo "Web Docker image built successfully: $(WEB_IMAGE):$(VERSION)"
|
||||||
|
|
||||||
build-api:
|
build-api:
|
||||||
@ -126,14 +191,17 @@ help:
|
|||||||
@echo " make prepare-docker - Set up Docker middleware"
|
@echo " make prepare-docker - Set up Docker middleware"
|
||||||
@echo " make prepare-web - Set up web environment"
|
@echo " make prepare-web - Set up web environment"
|
||||||
@echo " make prepare-api - Set up API environment"
|
@echo " make prepare-api - Set up API environment"
|
||||||
@echo " make dev-clean - Stop Docker middleware containers"
|
@echo " make dev-clean - Stop Docker middleware containers and remove dev data"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Backend Code Quality:"
|
@echo "Backend Code Quality:"
|
||||||
@echo " make format - Format code with ruff"
|
@echo " make format - Format code with ruff"
|
||||||
@echo " make check - Check code with ruff"
|
@echo " make check - Check code with ruff"
|
||||||
@echo " make lint - Format, fix, and lint code (ruff, imports, dotenv)"
|
@echo " make lint - Format, fix, and lint code (ruff, imports, dotenv)"
|
||||||
@echo " make type-check - Run type checks (basedpyright, pyrefly, mypy)"
|
@echo " make api-contract-lint - Check Flask response docs against returned schemas"
|
||||||
|
@echo " make type-check - Run type checks (pyrefly, mypy)"
|
||||||
|
@echo " make type-check-core - Run core type checks (pyrefly, mypy)"
|
||||||
@echo " make test - Run backend unit tests (or TARGET_TESTS=./api/tests/<target_tests>)"
|
@echo " make test - Run backend unit tests (or TARGET_TESTS=./api/tests/<target_tests>)"
|
||||||
|
@echo " make test-all - Run full backend tests, including Docker-backed suites"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Docker Build Targets:"
|
@echo "Docker Build Targets:"
|
||||||
@echo " make build-web - Build web Docker image"
|
@echo " make build-web - Build web Docker image"
|
||||||
@ -143,4 +211,4 @@ help:
|
|||||||
@echo " make build-push-all - Build and push all Docker images"
|
@echo " make build-push-all - Build and push all Docker images"
|
||||||
|
|
||||||
# Phony targets
|
# Phony targets
|
||||||
.PHONY: build-web build-api push-web push-api build-all push-all build-push-all dev-setup prepare-docker prepare-web prepare-api dev-clean help format check lint type-check test
|
.PHONY: build-web build-api push-web push-api build-all push-all build-push-all dev-setup prepare-docker prepare-web prepare-api dev-clean help format check lint api-contract-lint type-check test test-all
|
||||||
|
|||||||
21
README.md
21
README.md
@ -53,7 +53,11 @@
|
|||||||
<a href="./docs/tr-TR/README.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
<a href="./docs/tr-TR/README.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||||
<a href="./docs/vi-VN/README.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
<a href="./docs/vi-VN/README.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||||
<a href="./docs/de-DE/README.md"><img alt="README in Deutsch" src="https://img.shields.io/badge/German-d9d9d9"></a>
|
<a href="./docs/de-DE/README.md"><img alt="README in Deutsch" src="https://img.shields.io/badge/German-d9d9d9"></a>
|
||||||
|
<a href="./docs/it-IT/README.md"><img alt="README in Italiano" src="https://img.shields.io/badge/Italiano-d9d9d9"></a>
|
||||||
|
<a href="./docs/pt-BR/README.md"><img alt="README em Português do Brasil" src="https://img.shields.io/badge/Portugu%C3%AAs%20do%20Brasil-d9d9d9"></a>
|
||||||
|
<a href="./docs/sl-SI/README.md"><img alt="README Slovenščina" src="https://img.shields.io/badge/Sloven%C5%A1%C4%8Dina-d9d9d9"></a>
|
||||||
<a href="./docs/bn-BD/README.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
<a href="./docs/bn-BD/README.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||||
|
<a href="./docs/hi-IN/README.md"><img alt="README in हिन्दी" src="https://img.shields.io/badge/Hindi-d9d9d9"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Dify is an open-source LLM app development platform. Its intuitive interface combines AI workflow, RAG pipeline, agent capabilities, model management, observability features (including [Opik](https://www.comet.com/docs/opik/integrations/dify), [Langfuse](https://docs.langfuse.com), and [Arize Phoenix](https://docs.arize.com/phoenix)) and more, letting you quickly go from prototype to production. Here's a list of the core features:
|
Dify is an open-source LLM app development platform. Its intuitive interface combines AI workflow, RAG pipeline, agent capabilities, model management, observability features (including [Opik](https://www.comet.com/docs/opik/integrations/dify), [Langfuse](https://docs.langfuse.com), and [Arize Phoenix](https://docs.arize.com/phoenix)) and more, letting you quickly go from prototype to production. Here's a list of the core features:
|
||||||
@ -133,20 +137,7 @@ Star Dify on GitHub and be instantly notified of new releases.
|
|||||||
|
|
||||||
### Custom configurations
|
### Custom configurations
|
||||||
|
|
||||||
If you need to customize the configuration, please refer to the comments in our [.env.example](docker/.env.example) file and update the corresponding values in your `.env` file. Additionally, you might need to make adjustments to the `docker-compose.yaml` file itself, such as changing image versions, port mappings, or volume mounts, based on your specific deployment environment and requirements. After making any changes, please re-run `docker compose up -d`. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments).
|
If you need to customize the configuration, edit `docker/.env`. The essential startup defaults live in [`docker/.env.example`](docker/.env.example), and optional advanced variables are split under `docker/envs/` by theme. After making any changes, re-run `docker compose up -d` from the `docker` directory. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments).
|
||||||
|
|
||||||
#### Customizing Suggested Questions
|
|
||||||
|
|
||||||
You can now customize the "Suggested Questions After Answer" feature to better fit your use case. For example, to generate longer, more technical questions:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# In your .env file
|
|
||||||
SUGGESTED_QUESTIONS_PROMPT='Please help me predict the five most likely technical follow-up questions a developer would ask. Focus on implementation details, best practices, and architecture considerations. Keep each question between 40-60 characters. Output must be JSON array: ["question1","question2","question3","question4","question5"]'
|
|
||||||
SUGGESTED_QUESTIONS_MAX_TOKENS=512
|
|
||||||
SUGGESTED_QUESTIONS_TEMPERATURE=0.3
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [Suggested Questions Configuration Guide](docs/suggested-questions-configuration.md) for detailed examples and usage instructions.
|
|
||||||
|
|
||||||
### Metrics Monitoring with Grafana
|
### Metrics Monitoring with Grafana
|
||||||
|
|
||||||
@ -156,7 +147,7 @@ Import the dashboard to Grafana, using Dify's PostgreSQL database as data source
|
|||||||
|
|
||||||
### Deployment with Kubernetes
|
### Deployment with Kubernetes
|
||||||
|
|
||||||
If you'd like to configure a highly-available setup, there are community-contributed [Helm Charts](https://helm.sh/) and YAML files which allow Dify to be deployed on Kubernetes.
|
If you'd like to configure a highly available setup, there are community-contributed [Helm Charts](https://helm.sh/) and YAML files which allow Dify to be deployed on Kubernetes.
|
||||||
|
|
||||||
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
|
|||||||
@ -34,7 +34,7 @@ TRIGGER_URL=http://localhost:5001
|
|||||||
FILES_ACCESS_TIMEOUT=300
|
FILES_ACCESS_TIMEOUT=300
|
||||||
|
|
||||||
# Collaboration mode toggle
|
# Collaboration mode toggle
|
||||||
ENABLE_COLLABORATION_MODE=false
|
ENABLE_COLLABORATION_MODE=true
|
||||||
|
|
||||||
# Access token expiration time in minutes
|
# Access token expiration time in minutes
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||||
@ -60,6 +60,9 @@ REDIS_SSL_CERTFILE=
|
|||||||
REDIS_SSL_KEYFILE=
|
REDIS_SSL_KEYFILE=
|
||||||
# Path to client private key file for SSL authentication
|
# Path to client private key file for SSL authentication
|
||||||
REDIS_DB=0
|
REDIS_DB=0
|
||||||
|
# Optional global prefix for Redis keys, topics, streams, and Celery Redis transport artifacts.
|
||||||
|
# Leave empty to preserve current unprefixed behavior.
|
||||||
|
REDIS_KEY_PREFIX=
|
||||||
|
|
||||||
# redis Sentinel configuration.
|
# redis Sentinel configuration.
|
||||||
REDIS_USE_SENTINEL=false
|
REDIS_USE_SENTINEL=false
|
||||||
@ -74,10 +77,21 @@ REDIS_USE_CLUSTERS=false
|
|||||||
REDIS_CLUSTERS=
|
REDIS_CLUSTERS=
|
||||||
REDIS_CLUSTERS_PASSWORD=
|
REDIS_CLUSTERS_PASSWORD=
|
||||||
|
|
||||||
|
REDIS_RETRY_RETRIES=3
|
||||||
|
REDIS_RETRY_BACKOFF_BASE=1.0
|
||||||
|
REDIS_RETRY_BACKOFF_CAP=10.0
|
||||||
|
REDIS_SOCKET_TIMEOUT=5.0
|
||||||
|
REDIS_SOCKET_CONNECT_TIMEOUT=5.0
|
||||||
|
REDIS_HEALTH_CHECK_INTERVAL=30
|
||||||
|
|
||||||
# celery configuration
|
# celery configuration
|
||||||
CELERY_BROKER_URL=redis://:difyai123456@localhost:${REDIS_PORT}/1
|
CELERY_BROKER_URL=redis://:difyai123456@localhost:${REDIS_PORT}/1
|
||||||
CELERY_BACKEND=redis
|
CELERY_BACKEND=redis
|
||||||
|
|
||||||
|
# Ops trace retry configuration
|
||||||
|
OPS_TRACE_RETRYABLE_DISPATCH_MAX_RETRIES=60
|
||||||
|
OPS_TRACE_RETRYABLE_DISPATCH_DELAY_SECONDS=5
|
||||||
|
|
||||||
# Database configuration
|
# Database configuration
|
||||||
DB_TYPE=postgresql
|
DB_TYPE=postgresql
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
@ -88,6 +102,8 @@ DB_DATABASE=dify
|
|||||||
|
|
||||||
SQLALCHEMY_POOL_PRE_PING=true
|
SQLALCHEMY_POOL_PRE_PING=true
|
||||||
SQLALCHEMY_POOL_TIMEOUT=30
|
SQLALCHEMY_POOL_TIMEOUT=30
|
||||||
|
# Connection pool reset behavior on return
|
||||||
|
SQLALCHEMY_POOL_RESET_ON_RETURN=rollback
|
||||||
|
|
||||||
# Storage configuration
|
# Storage configuration
|
||||||
# use for store upload files, private keys...
|
# use for store upload files, private keys...
|
||||||
@ -105,6 +121,7 @@ S3_BUCKET_NAME=your-bucket-name
|
|||||||
S3_ACCESS_KEY=your-access-key
|
S3_ACCESS_KEY=your-access-key
|
||||||
S3_SECRET_KEY=your-secret-key
|
S3_SECRET_KEY=your-secret-key
|
||||||
S3_REGION=your-region
|
S3_REGION=your-region
|
||||||
|
S3_ADDRESS_STYLE=auto
|
||||||
|
|
||||||
# Workflow run and Conversation archive storage (S3-compatible)
|
# Workflow run and Conversation archive storage (S3-compatible)
|
||||||
ARCHIVE_STORAGE_ENABLED=false
|
ARCHIVE_STORAGE_ENABLED=false
|
||||||
@ -130,7 +147,8 @@ ALIYUN_OSS_AUTH_VERSION=v1
|
|||||||
ALIYUN_OSS_REGION=your-region
|
ALIYUN_OSS_REGION=your-region
|
||||||
# Don't start with '/'. OSS doesn't support leading slash in object names.
|
# Don't start with '/'. OSS doesn't support leading slash in object names.
|
||||||
ALIYUN_OSS_PATH=your-path
|
ALIYUN_OSS_PATH=your-path
|
||||||
ALIYUN_CLOUDBOX_ID=your-cloudbox-id
|
# Optional CloudBox ID for Aliyun OSS, DO NOT enable it if you are not using CloudBox.
|
||||||
|
#ALIYUN_CLOUDBOX_ID=your-cloudbox-id
|
||||||
|
|
||||||
# Google Storage configuration
|
# Google Storage configuration
|
||||||
GOOGLE_STORAGE_BUCKET_NAME=your-bucket-name
|
GOOGLE_STORAGE_BUCKET_NAME=your-bucket-name
|
||||||
@ -369,7 +387,7 @@ VIKINGDB_ACCESS_KEY=your-ak
|
|||||||
VIKINGDB_SECRET_KEY=your-sk
|
VIKINGDB_SECRET_KEY=your-sk
|
||||||
VIKINGDB_REGION=cn-shanghai
|
VIKINGDB_REGION=cn-shanghai
|
||||||
VIKINGDB_HOST=api-vikingdb.xxx.volces.com
|
VIKINGDB_HOST=api-vikingdb.xxx.volces.com
|
||||||
VIKINGDB_SCHEMA=http
|
VIKINGDB_SCHEME=http
|
||||||
VIKINGDB_CONNECTION_TIMEOUT=30
|
VIKINGDB_CONNECTION_TIMEOUT=30
|
||||||
VIKINGDB_SOCKET_TIMEOUT=30
|
VIKINGDB_SOCKET_TIMEOUT=30
|
||||||
|
|
||||||
@ -420,8 +438,6 @@ UPLOAD_FILE_EXTENSION_BLACKLIST=
|
|||||||
|
|
||||||
# Model configuration
|
# Model configuration
|
||||||
MULTIMODAL_SEND_FORMAT=base64
|
MULTIMODAL_SEND_FORMAT=base64
|
||||||
PROMPT_GENERATION_MAX_TOKENS=512
|
|
||||||
CODE_GENERATION_MAX_TOKENS=1024
|
|
||||||
PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false
|
PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false
|
||||||
|
|
||||||
# Mail configuration, support: resend, smtp, sendgrid
|
# Mail configuration, support: resend, smtp, sendgrid
|
||||||
@ -541,7 +557,7 @@ MAX_VARIABLE_SIZE=204800
|
|||||||
|
|
||||||
# GraphEngine Worker Pool Configuration
|
# GraphEngine Worker Pool Configuration
|
||||||
# Minimum number of workers per GraphEngine instance (default: 1)
|
# Minimum number of workers per GraphEngine instance (default: 1)
|
||||||
GRAPH_ENGINE_MIN_WORKERS=1
|
GRAPH_ENGINE_MIN_WORKERS=3
|
||||||
# Maximum number of workers per GraphEngine instance (default: 10)
|
# Maximum number of workers per GraphEngine instance (default: 10)
|
||||||
GRAPH_ENGINE_MAX_WORKERS=10
|
GRAPH_ENGINE_MAX_WORKERS=10
|
||||||
# Queue depth threshold that triggers worker scale up (default: 3)
|
# Queue depth threshold that triggers worker scale up (default: 3)
|
||||||
@ -641,6 +657,7 @@ PLUGIN_REMOTE_INSTALL_PORT=5003
|
|||||||
PLUGIN_REMOTE_INSTALL_HOST=localhost
|
PLUGIN_REMOTE_INSTALL_HOST=localhost
|
||||||
PLUGIN_MAX_PACKAGE_SIZE=15728640
|
PLUGIN_MAX_PACKAGE_SIZE=15728640
|
||||||
PLUGIN_MODEL_SCHEMA_CACHE_TTL=3600
|
PLUGIN_MODEL_SCHEMA_CACHE_TTL=3600
|
||||||
|
PLUGIN_MODEL_PROVIDERS_CACHE_TTL=86400
|
||||||
INNER_API_KEY_FOR_PLUGIN=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
INNER_API_KEY_FOR_PLUGIN=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
||||||
|
|
||||||
# Marketplace configuration
|
# Marketplace configuration
|
||||||
@ -702,22 +719,6 @@ SWAGGER_UI_PATH=/swagger-ui.html
|
|||||||
# Set to false to export dataset IDs as plain text for easier cross-environment import
|
# Set to false to export dataset IDs as plain text for easier cross-environment import
|
||||||
DSL_EXPORT_ENCRYPT_DATASET_ID=true
|
DSL_EXPORT_ENCRYPT_DATASET_ID=true
|
||||||
|
|
||||||
# Suggested Questions After Answer Configuration
|
|
||||||
# These environment variables allow customization of the suggested questions feature
|
|
||||||
#
|
|
||||||
# Custom prompt for generating suggested questions (optional)
|
|
||||||
# If not set, uses the default prompt that generates 3 questions under 20 characters each
|
|
||||||
# Example: "Please help me predict the five most likely technical follow-up questions a developer would ask. Focus on implementation details, best practices, and architecture considerations. Keep each question between 40-60 characters. Output must be JSON array: [\"question1\",\"question2\",\"question3\",\"question4\",\"question5\"]"
|
|
||||||
# SUGGESTED_QUESTIONS_PROMPT=
|
|
||||||
|
|
||||||
# Maximum number of tokens for suggested questions generation (default: 256)
|
|
||||||
# Adjust this value for longer questions or more questions
|
|
||||||
# SUGGESTED_QUESTIONS_MAX_TOKENS=256
|
|
||||||
|
|
||||||
# Temperature for suggested questions generation (default: 0.0)
|
|
||||||
# Higher values (0.5-1.0) produce more creative questions, lower values (0.0-0.3) produce more focused questions
|
|
||||||
# SUGGESTED_QUESTIONS_TEMPERATURE=0
|
|
||||||
|
|
||||||
# Tenant isolated task queue configuration
|
# Tenant isolated task queue configuration
|
||||||
TENANT_ISOLATED_TASK_CONCURRENCY=1
|
TENANT_ISOLATED_TASK_CONCURRENCY=1
|
||||||
|
|
||||||
@ -747,34 +748,6 @@ SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_MAX_INTERVAL=200
|
|||||||
SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS=30
|
SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS=30
|
||||||
SANDBOX_EXPIRED_RECORDS_CLEAN_TASK_LOCK_TTL=90000
|
SANDBOX_EXPIRED_RECORDS_CLEAN_TASK_LOCK_TTL=90000
|
||||||
|
|
||||||
# Sandbox Dify CLI configuration
|
|
||||||
# Directory containing dify CLI binaries (dify-cli-<os>-<arch>). Defaults to api/bin when unset.
|
|
||||||
SANDBOX_DIFY_CLI_ROOT=
|
|
||||||
|
|
||||||
# CLI API URL for sandbox (dify-sandbox or e2b) to call back to Dify API.
|
|
||||||
# This URL must be accessible from the sandbox environment.
|
|
||||||
# For local development: use http://localhost:5001 or http://127.0.0.1:5001
|
|
||||||
# For middleware docker stack (api on host): keep localhost/127.0.0.1 and use agentbox via 127.0.0.1:2222
|
|
||||||
# For Docker deployment: use http://api:5001 (internal Docker network)
|
|
||||||
# For external sandbox (e.g., e2b): use a publicly accessible URL
|
|
||||||
CLI_API_URL=http://localhost:5001
|
|
||||||
|
|
||||||
# Base URL for storage file ticket API endpoints (upload/download).
|
|
||||||
# Used by sandbox containers (internal or external like e2b) that need an absolute,
|
|
||||||
# routable address to reach the Dify API file endpoints.
|
|
||||||
# Required for sandbox runtime file access.
|
|
||||||
# For local development: http://localhost:5001
|
|
||||||
# For all-in-one Docker deployment with nginx: http://localhost
|
|
||||||
# For public/remote sandbox environments (e.g., e2b): use a public domain or IP
|
|
||||||
FILES_API_URL=http://localhost:5001
|
|
||||||
|
|
||||||
# Optional defaults for SSH sandbox provider setup (for manual config/CLI usage).
|
|
||||||
# Middleware/local dev usually uses 127.0.0.1:2222; full docker deployment usually uses agentbox:22.
|
|
||||||
SSH_SANDBOX_HOST=127.0.0.1
|
|
||||||
SSH_SANDBOX_PORT=2222
|
|
||||||
SSH_SANDBOX_USERNAME=agentbox
|
|
||||||
SSH_SANDBOX_PASSWORD=agentbox
|
|
||||||
SSH_SANDBOX_BASE_WORKING_PATH=/workspace/sandboxes
|
|
||||||
|
|
||||||
# Redis URL used for event bus between API and
|
# Redis URL used for event bus between API and
|
||||||
# celery worker
|
# celery worker
|
||||||
@ -795,6 +768,7 @@ EVENT_BUS_REDIS_CHANNEL_TYPE=pubsub
|
|||||||
# Whether to use Redis cluster mode while use redis as event bus.
|
# Whether to use Redis cluster mode while use redis as event bus.
|
||||||
# It's highly recommended to enable this for large deployments.
|
# It's highly recommended to enable this for large deployments.
|
||||||
EVENT_BUS_REDIS_USE_CLUSTERS=false
|
EVENT_BUS_REDIS_USE_CLUSTERS=false
|
||||||
|
EVENT_BUS_LISTENER_JOIN_TIMEOUT_MS=2000
|
||||||
|
|
||||||
# Whether to Enable human input timeout check task
|
# Whether to Enable human input timeout check task
|
||||||
ENABLE_HUMAN_INPUT_TIMEOUT_TASK=true
|
ENABLE_HUMAN_INPUT_TIMEOUT_TASK=true
|
||||||
|
|||||||
@ -1,242 +1,14 @@
|
|||||||
[importlinter]
|
[importlinter]
|
||||||
root_packages =
|
root_packages =
|
||||||
core
|
core
|
||||||
dify_graph
|
constants
|
||||||
|
context
|
||||||
configs
|
configs
|
||||||
controllers
|
controllers
|
||||||
extensions
|
extensions
|
||||||
|
factories
|
||||||
|
libs
|
||||||
models
|
models
|
||||||
tasks
|
tasks
|
||||||
services
|
services
|
||||||
include_external_packages = True
|
include_external_packages = True
|
||||||
|
|
||||||
[importlinter:contract:workflow]
|
|
||||||
name = Workflow
|
|
||||||
type=layers
|
|
||||||
layers =
|
|
||||||
graph_engine
|
|
||||||
graph_events
|
|
||||||
graph
|
|
||||||
nodes
|
|
||||||
node_events
|
|
||||||
runtime
|
|
||||||
entities
|
|
||||||
containers =
|
|
||||||
dify_graph
|
|
||||||
ignore_imports =
|
|
||||||
dify_graph.nodes.base.node -> dify_graph.graph_events
|
|
||||||
dify_graph.nodes.iteration.iteration_node -> dify_graph.graph_events
|
|
||||||
dify_graph.nodes.loop.loop_node -> dify_graph.graph_events
|
|
||||||
|
|
||||||
dify_graph.nodes.iteration.iteration_node -> dify_graph.graph_engine
|
|
||||||
dify_graph.nodes.loop.loop_node -> dify_graph.graph_engine
|
|
||||||
# TODO(QuantumGhost): fix the import violation later
|
|
||||||
dify_graph.entities.pause_reason -> dify_graph.nodes.human_input.entities
|
|
||||||
|
|
||||||
dify_graph.nodes.base.node -> core.workflow.node_factory
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.workflow.node_factory
|
|
||||||
dify_graph.file.file_manager -> models.model
|
|
||||||
dify_graph.file.file_manager -> models.tools
|
|
||||||
dify_graph.file.file_manager -> extensions.ext_database
|
|
||||||
|
|
||||||
[importlinter:contract:workflow-infrastructure-dependencies]
|
|
||||||
name = Workflow Infrastructure Dependencies
|
|
||||||
type = forbidden
|
|
||||||
source_modules =
|
|
||||||
dify_graph
|
|
||||||
forbidden_modules =
|
|
||||||
extensions.ext_database
|
|
||||||
extensions.ext_redis
|
|
||||||
allow_indirect_imports = True
|
|
||||||
ignore_imports =
|
|
||||||
dify_graph.nodes.llm.node -> extensions.ext_database
|
|
||||||
dify_graph.model_runtime.model_providers.__base.ai_model -> extensions.ext_redis
|
|
||||||
dify_graph.model_runtime.model_providers.model_provider_factory -> extensions.ext_redis
|
|
||||||
dify_graph.file.file_manager -> extensions.ext_database
|
|
||||||
dify_graph.nodes.llm.llm_utils -> extensions.ext_database
|
|
||||||
|
|
||||||
[importlinter:contract:workflow-external-imports]
|
|
||||||
name = Workflow External Imports
|
|
||||||
type = forbidden
|
|
||||||
source_modules =
|
|
||||||
dify_graph
|
|
||||||
forbidden_modules =
|
|
||||||
configs
|
|
||||||
controllers
|
|
||||||
extensions
|
|
||||||
models
|
|
||||||
services
|
|
||||||
tasks
|
|
||||||
core.agent
|
|
||||||
core.app
|
|
||||||
core.base
|
|
||||||
core.callback_handler
|
|
||||||
core.datasource
|
|
||||||
core.db
|
|
||||||
core.entities
|
|
||||||
core.errors
|
|
||||||
core.extension
|
|
||||||
core.external_data_tool
|
|
||||||
core.file
|
|
||||||
core.helper
|
|
||||||
core.hosting_configuration
|
|
||||||
core.indexing_runner
|
|
||||||
core.llm_generator
|
|
||||||
core.logging
|
|
||||||
core.mcp
|
|
||||||
core.memory
|
|
||||||
core.moderation
|
|
||||||
core.ops
|
|
||||||
core.plugin
|
|
||||||
core.prompt
|
|
||||||
core.provider_manager
|
|
||||||
core.rag
|
|
||||||
core.repositories
|
|
||||||
core.schemas
|
|
||||||
core.tools
|
|
||||||
core.trigger
|
|
||||||
core.variables
|
|
||||||
ignore_imports =
|
|
||||||
dify_graph.nodes.llm.llm_utils -> core.model_manager
|
|
||||||
dify_graph.nodes.llm.protocols -> core.model_manager
|
|
||||||
dify_graph.nodes.llm.llm_utils -> dify_graph.model_runtime.model_providers.__base.large_language_model
|
|
||||||
dify_graph.nodes.llm.node -> core.tools.signature
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.callback_handler.workflow_tool_callback_handler
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.tools.tool_engine
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.tools.tool_manager
|
|
||||||
dify_graph.nodes.parameter_extractor.parameter_extractor_node -> core.prompt.advanced_prompt_transform
|
|
||||||
dify_graph.nodes.parameter_extractor.parameter_extractor_node -> core.prompt.simple_prompt_transform
|
|
||||||
dify_graph.nodes.parameter_extractor.parameter_extractor_node -> dify_graph.model_runtime.model_providers.__base.large_language_model
|
|
||||||
dify_graph.nodes.question_classifier.question_classifier_node -> core.prompt.simple_prompt_transform
|
|
||||||
dify_graph.nodes.parameter_extractor.parameter_extractor_node -> core.model_manager
|
|
||||||
dify_graph.nodes.question_classifier.question_classifier_node -> core.model_manager
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.tools.utils.message_transformer
|
|
||||||
dify_graph.nodes.llm.node -> core.llm_generator.output_parser.errors
|
|
||||||
dify_graph.nodes.llm.node -> core.llm_generator.output_parser.file_ref
|
|
||||||
dify_graph.nodes.llm.node -> core.llm_generator.output_parser.structured_output
|
|
||||||
dify_graph.nodes.llm.node -> core.model_manager
|
|
||||||
dify_graph.nodes.llm.entities -> core.prompt.entities.advanced_prompt_entities
|
|
||||||
dify_graph.nodes.llm.node -> core.prompt.entities.advanced_prompt_entities
|
|
||||||
dify_graph.nodes.llm.node -> core.prompt.utils.prompt_message_util
|
|
||||||
dify_graph.nodes.parameter_extractor.entities -> core.prompt.entities.advanced_prompt_entities
|
|
||||||
dify_graph.nodes.parameter_extractor.parameter_extractor_node -> core.prompt.entities.advanced_prompt_entities
|
|
||||||
dify_graph.nodes.parameter_extractor.parameter_extractor_node -> core.prompt.utils.prompt_message_util
|
|
||||||
dify_graph.nodes.question_classifier.entities -> core.prompt.entities.advanced_prompt_entities
|
|
||||||
dify_graph.nodes.question_classifier.question_classifier_node -> core.prompt.utils.prompt_message_util
|
|
||||||
dify_graph.nodes.llm.node -> models.dataset
|
|
||||||
dify_graph.nodes.llm.file_saver -> core.tools.signature
|
|
||||||
dify_graph.nodes.llm.file_saver -> core.tools.tool_file_manager
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.tools.errors
|
|
||||||
dify_graph.nodes.llm.node -> extensions.ext_database
|
|
||||||
dify_graph.nodes.llm.node -> models.model
|
|
||||||
dify_graph.nodes.llm.node -> configs
|
|
||||||
dify_graph.nodes.llm.node -> core.agent.entities
|
|
||||||
dify_graph.nodes.llm.node -> core.agent.patterns
|
|
||||||
dify_graph.nodes.llm.node -> core.app.entities.app_invoke_entities
|
|
||||||
dify_graph.nodes.llm.node -> core.helper.code_executor
|
|
||||||
dify_graph.nodes.llm.node -> core.memory.base
|
|
||||||
dify_graph.nodes.llm.node -> core.sandbox
|
|
||||||
dify_graph.nodes.llm.node -> core.sandbox.bash.session
|
|
||||||
dify_graph.nodes.llm.node -> core.sandbox.entities.config
|
|
||||||
dify_graph.nodes.llm.node -> core.skill.assembler
|
|
||||||
dify_graph.nodes.llm.node -> core.skill.constants
|
|
||||||
dify_graph.nodes.llm.node -> core.skill.entities.skill_bundle
|
|
||||||
dify_graph.nodes.llm.node -> core.skill.entities.skill_document
|
|
||||||
dify_graph.nodes.llm.node -> core.skill.entities.skill_metadata
|
|
||||||
dify_graph.nodes.llm.node -> core.skill.entities.tool_dependencies
|
|
||||||
dify_graph.nodes.llm.node -> core.tools.tool_file_manager
|
|
||||||
dify_graph.nodes.llm.node -> core.tools.tool_manager
|
|
||||||
dify_graph.nodes.tool.tool_node -> services
|
|
||||||
dify_graph.model_runtime.model_providers.__base.ai_model -> configs
|
|
||||||
dify_graph.model_runtime.model_providers.__base.ai_model -> extensions.ext_redis
|
|
||||||
dify_graph.model_runtime.model_providers.__base.large_language_model -> configs
|
|
||||||
dify_graph.model_runtime.model_providers.__base.text_embedding_model -> core.entities.embedding_type
|
|
||||||
dify_graph.model_runtime.model_providers.model_provider_factory -> configs
|
|
||||||
dify_graph.model_runtime.model_providers.model_provider_factory -> extensions.ext_redis
|
|
||||||
dify_graph.model_runtime.model_providers.model_provider_factory -> models.provider_ids
|
|
||||||
dify_graph.file.file_manager -> configs
|
|
||||||
dify_graph.file.file_manager -> extensions.ext_database
|
|
||||||
dify_graph.file.file_manager -> models.model
|
|
||||||
dify_graph.file.file_manager -> models.tools
|
|
||||||
dify_graph.nodes.llm.llm_utils -> core.app.llm.model_access
|
|
||||||
dify_graph.nodes.llm.llm_utils -> core.app.llm.quota
|
|
||||||
dify_graph.nodes.llm.llm_utils -> core.memory
|
|
||||||
dify_graph.nodes.llm.llm_utils -> core.memory.base
|
|
||||||
dify_graph.nodes.llm.llm_utils -> extensions.ext_database
|
|
||||||
dify_graph.nodes.llm.llm_utils -> models.model
|
|
||||||
dify_graph.nodes.llm.llm_utils -> core.prompt.entities.advanced_prompt_entities
|
|
||||||
dify_graph.nodes.llm.entities -> core.agent.entities
|
|
||||||
dify_graph.nodes.base.node -> core.workflow.node_factory
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.workflow.node_factory
|
|
||||||
|
|
||||||
[importlinter:contract:rsc]
|
|
||||||
name = RSC
|
|
||||||
type = layers
|
|
||||||
layers =
|
|
||||||
graph_engine
|
|
||||||
response_coordinator
|
|
||||||
containers =
|
|
||||||
dify_graph.graph_engine
|
|
||||||
|
|
||||||
[importlinter:contract:worker]
|
|
||||||
name = Worker
|
|
||||||
type = layers
|
|
||||||
layers =
|
|
||||||
graph_engine
|
|
||||||
worker
|
|
||||||
containers =
|
|
||||||
dify_graph.graph_engine
|
|
||||||
|
|
||||||
[importlinter:contract:graph-engine-architecture]
|
|
||||||
name = Graph Engine Architecture
|
|
||||||
type = layers
|
|
||||||
layers =
|
|
||||||
graph_engine
|
|
||||||
orchestration
|
|
||||||
command_processing
|
|
||||||
event_management
|
|
||||||
error_handler
|
|
||||||
graph_traversal
|
|
||||||
graph_state_manager
|
|
||||||
worker_management
|
|
||||||
domain
|
|
||||||
containers =
|
|
||||||
dify_graph.graph_engine
|
|
||||||
|
|
||||||
[importlinter:contract:domain-isolation]
|
|
||||||
name = Domain Model Isolation
|
|
||||||
type = forbidden
|
|
||||||
source_modules =
|
|
||||||
dify_graph.graph_engine.domain
|
|
||||||
forbidden_modules =
|
|
||||||
dify_graph.graph_engine.worker_management
|
|
||||||
dify_graph.graph_engine.command_channels
|
|
||||||
dify_graph.graph_engine.layers
|
|
||||||
dify_graph.graph_engine.protocols
|
|
||||||
|
|
||||||
[importlinter:contract:worker-management]
|
|
||||||
name = Worker Management
|
|
||||||
type = forbidden
|
|
||||||
source_modules =
|
|
||||||
dify_graph.graph_engine.worker_management
|
|
||||||
forbidden_modules =
|
|
||||||
dify_graph.graph_engine.orchestration
|
|
||||||
dify_graph.graph_engine.command_processing
|
|
||||||
dify_graph.graph_engine.event_management
|
|
||||||
|
|
||||||
|
|
||||||
[importlinter:contract:graph-traversal-components]
|
|
||||||
name = Graph Traversal Components
|
|
||||||
type = layers
|
|
||||||
layers =
|
|
||||||
edge_processor
|
|
||||||
skip_propagator
|
|
||||||
containers =
|
|
||||||
dify_graph.graph_engine.graph_traversal
|
|
||||||
|
|
||||||
[importlinter:contract:command-channels]
|
|
||||||
name = Command Channels Independence
|
|
||||||
type = independence
|
|
||||||
modules =
|
|
||||||
dify_graph.graph_engine.command_channels.in_memory_channel
|
|
||||||
dify_graph.graph_engine.command_channels.redis_channel
|
|
||||||
|
|||||||
@ -69,8 +69,6 @@ ignore = [
|
|||||||
"FURB152", # math-constant
|
"FURB152", # math-constant
|
||||||
"UP007", # non-pep604-annotation
|
"UP007", # non-pep604-annotation
|
||||||
"UP032", # f-string
|
"UP032", # f-string
|
||||||
"UP045", # non-pep604-annotation-optional
|
|
||||||
"B005", # strip-with-multi-characters
|
|
||||||
"B006", # mutable-argument-default
|
"B006", # mutable-argument-default
|
||||||
"B007", # unused-loop-control-variable
|
"B007", # unused-loop-control-variable
|
||||||
"B026", # star-arg-unpacking-after-keyword-arg
|
"B026", # star-arg-unpacking-after-keyword-arg
|
||||||
@ -84,7 +82,6 @@ ignore = [
|
|||||||
"SIM102", # collapsible-if
|
"SIM102", # collapsible-if
|
||||||
"SIM103", # needless-bool
|
"SIM103", # needless-bool
|
||||||
"SIM105", # suppressible-exception
|
"SIM105", # suppressible-exception
|
||||||
"SIM107", # return-in-try-except-finally
|
|
||||||
"SIM108", # if-else-block-instead-of-if-exp
|
"SIM108", # if-else-block-instead-of-if-exp
|
||||||
"SIM113", # enumerate-for-loop
|
"SIM113", # enumerate-for-loop
|
||||||
"SIM117", # multiple-with-statements
|
"SIM117", # multiple-with-statements
|
||||||
@ -93,38 +90,22 @@ ignore = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[lint.per-file-ignores]
|
[lint.per-file-ignores]
|
||||||
"__init__.py" = [
|
|
||||||
"F401", # unused-import
|
|
||||||
"F811", # redefined-while-unused
|
|
||||||
]
|
|
||||||
"configs/*" = [
|
"configs/*" = [
|
||||||
"N802", # invalid-function-name
|
"N802", # invalid-function-name
|
||||||
]
|
]
|
||||||
"dify_graph/model_runtime/callbacks/base_callback.py" = ["T201"]
|
|
||||||
"core/workflow/callbacks/workflow_logging_callback.py" = ["T201"]
|
|
||||||
"libs/gmpy2_pkcs10aep_cipher.py" = [
|
"libs/gmpy2_pkcs10aep_cipher.py" = [
|
||||||
"N803", # invalid-argument-name
|
"N803", # invalid-argument-name
|
||||||
]
|
]
|
||||||
"tests/*" = [
|
"tests/*" = [
|
||||||
"F811", # redefined-while-unused
|
|
||||||
"T201", # allow print in tests,
|
"T201", # allow print in tests,
|
||||||
"S110", # allow ignoring exceptions in tests code (currently)
|
"S110", # allow ignoring exceptions in tests code (currently)
|
||||||
|
|
||||||
]
|
]
|
||||||
"controllers/console/explore/trial.py" = ["TID251"]
|
|
||||||
"controllers/console/human_input_form.py" = ["TID251"]
|
|
||||||
"controllers/web/human_input_form.py" = ["TID251"]
|
|
||||||
|
|
||||||
[lint.pyflakes]
|
|
||||||
allowed-unused-imports = [
|
|
||||||
"tests.integration_tests",
|
|
||||||
"tests.unit_tests",
|
|
||||||
]
|
|
||||||
|
|
||||||
[lint.flake8-tidy-imports]
|
|
||||||
|
|
||||||
[lint.flake8-tidy-imports.banned-api."flask_restx.reqparse"]
|
[lint.flake8-tidy-imports.banned-api."flask_restx.reqparse"]
|
||||||
msg = "Use Pydantic payload/query models instead of reqparse."
|
msg = "Use Pydantic payload/query models instead of reqparse."
|
||||||
|
|
||||||
[lint.flake8-tidy-imports.banned-api."flask_restx.reqparse.RequestParser"]
|
[lint.flake8-tidy-imports.banned-api."flask_restx.reqparse.RequestParser"]
|
||||||
msg = "Use Pydantic payload/query models instead of reqparse."
|
msg = "Use Pydantic payload/query models instead of reqparse."
|
||||||
|
|
||||||
|
[lint.isort]
|
||||||
|
known-first-party = ["graphon"]
|
||||||
18
api/.vscode/launch.json.example
vendored
18
api/.vscode/launch.json.example
vendored
@ -3,29 +3,21 @@
|
|||||||
"compounds": [
|
"compounds": [
|
||||||
{
|
{
|
||||||
"name": "Launch Flask and Celery",
|
"name": "Launch Flask and Celery",
|
||||||
"configurations": ["Python: Flask", "Python: Celery"]
|
"configurations": ["Python: API (gevent)", "Python: Celery"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Python: Flask",
|
"name": "Python: API (gevent)",
|
||||||
"consoleName": "Flask",
|
"consoleName": "API",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"python": "${workspaceFolder}/.venv/bin/python",
|
"python": "${workspaceFolder}/.venv/bin/python",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"envFile": ".env",
|
"envFile": ".env",
|
||||||
"module": "flask",
|
"program": "${workspaceFolder}/app.py",
|
||||||
"justMyCode": true,
|
"justMyCode": true,
|
||||||
"jinja": true,
|
"jinja": true
|
||||||
"env": {
|
|
||||||
"FLASK_APP": "app.py",
|
|
||||||
"GEVENT_SUPPORT": "True"
|
|
||||||
},
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"--port=5001"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Python: Celery",
|
"name": "Python: Celery",
|
||||||
|
|||||||
@ -180,6 +180,8 @@ Quick checks while iterating:
|
|||||||
- Format: `make format`
|
- Format: `make format`
|
||||||
- Lint (includes auto-fix): `make lint`
|
- Lint (includes auto-fix): `make lint`
|
||||||
- Type check: `make type-check`
|
- Type check: `make type-check`
|
||||||
|
- Unit tests: `make test`
|
||||||
|
- Full backend tests, including Docker-backed suites: `make test-all`
|
||||||
- Targeted tests: `make test TARGET_TESTS=./api/tests/<target_tests>`
|
- Targeted tests: `make test TARGET_TESTS=./api/tests/<target_tests>`
|
||||||
|
|
||||||
Before opening a PR / submitting:
|
Before opening a PR / submitting:
|
||||||
@ -193,6 +195,11 @@ Before opening a PR / submitting:
|
|||||||
- Controllers: parse input via Pydantic, invoke services, return serialised responses; no business logic.
|
- Controllers: parse input via Pydantic, invoke services, return serialised responses; no business logic.
|
||||||
- Services: coordinate repositories, providers, background tasks; keep side effects explicit.
|
- Services: coordinate repositories, providers, background tasks; keep side effects explicit.
|
||||||
- Document non-obvious behaviour with concise docstrings and comments.
|
- Document non-obvious behaviour with concise docstrings and comments.
|
||||||
|
- For `204 No Content` responses, return an empty body only; never return a dict, model, or other payload.
|
||||||
|
- For Flask-RESTX controller request, query, and response schemas, follow `controllers/API_SCHEMA_GUIDE.md`.
|
||||||
|
In short: use Pydantic models, document GET query params with `query_params_from_model(...)`, register response
|
||||||
|
DTOs with `register_response_schema_models(...)`, serialize response DTOs with `dump_response(...)`,
|
||||||
|
and avoid adding new legacy `ns.model(...)`, `@marshal_with(...)`, or GET `@ns.expect(...)` patterns.
|
||||||
|
|
||||||
### Miscellaneous
|
### Miscellaneous
|
||||||
|
|
||||||
|
|||||||
@ -21,9 +21,13 @@ RUN apt-get update \
|
|||||||
# for building gmpy2
|
# for building gmpy2
|
||||||
libmpfr-dev libmpc-dev
|
libmpfr-dev libmpc-dev
|
||||||
|
|
||||||
# Install Python dependencies
|
# Install Python dependencies (workspace members under providers/vdb/)
|
||||||
COPY pyproject.toml uv.lock ./
|
COPY api/pyproject.toml api/uv.lock ./
|
||||||
RUN uv sync --locked --no-dev
|
COPY api/providers ./providers
|
||||||
|
COPY dify-agent/pyproject.toml dify-agent/README.md /app/dify-agent/
|
||||||
|
COPY dify-agent/src /app/dify-agent/src
|
||||||
|
# Trust the checked-in lock during image builds; local path sources are copied from the repository context.
|
||||||
|
RUN uv sync --frozen --no-dev
|
||||||
|
|
||||||
# production stage
|
# production stage
|
||||||
FROM base AS production
|
FROM base AS production
|
||||||
@ -106,10 +110,10 @@ RUN python -c "import tiktoken; tiktoken.encoding_for_model('gpt2')" \
|
|||||||
&& chown -R dify:dify ${TIKTOKEN_CACHE_DIR}
|
&& chown -R dify:dify ${TIKTOKEN_CACHE_DIR}
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
COPY --chown=dify:dify . /app/api/
|
COPY --chown=dify:dify api /app/api/
|
||||||
|
|
||||||
# Prepare entrypoint script
|
# Prepare entrypoint script
|
||||||
COPY --chown=dify:dify --chmod=755 docker/entrypoint.sh /entrypoint.sh
|
COPY --chown=dify:dify --chmod=755 api/docker/entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
|
||||||
ARG COMMIT_SHA
|
ARG COMMIT_SHA
|
||||||
|
|||||||
25
api/Dockerfile.dockerignore
Normal file
25
api/Dockerfile.dockerignore
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
*
|
||||||
|
|
||||||
|
!api/
|
||||||
|
!api/**
|
||||||
|
!dify-agent/
|
||||||
|
!dify-agent/pyproject.toml
|
||||||
|
!dify-agent/README.md
|
||||||
|
!dify-agent/src/
|
||||||
|
!dify-agent/src/**
|
||||||
|
|
||||||
|
api/.venv
|
||||||
|
api/.venv/**
|
||||||
|
api/.env
|
||||||
|
api/*.env.*
|
||||||
|
api/.idea
|
||||||
|
api/.mypy_cache
|
||||||
|
api/.ruff_cache
|
||||||
|
api/storage/generate_files/*
|
||||||
|
api/storage/privkeys/*
|
||||||
|
api/storage/tools/*
|
||||||
|
api/storage/upload_files/*
|
||||||
|
api/logs
|
||||||
|
api/*.log*
|
||||||
|
**/__pycache__
|
||||||
|
**/*.pyc
|
||||||
@ -40,6 +40,8 @@ The scripts resolve paths relative to their location, so you can run them from a
|
|||||||
./dev/start-web
|
./dev/start-web
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`./dev/setup` and `./dev/start-web` install JavaScript dependencies through the repository root workspace, so you do not need a separate `cd web && pnpm install` step.
|
||||||
|
|
||||||
1. Set up your application by visiting `http://localhost:3000`.
|
1. Set up your application by visiting `http://localhost:3000`.
|
||||||
|
|
||||||
1. Start the worker service (async and scheduler tasks, runs from `api`).
|
1. Start the worker service (async and scheduler tasks, runs from `api`).
|
||||||
@ -54,86 +56,6 @@ The scripts resolve paths relative to their location, so you can run them from a
|
|||||||
./dev/start-beat
|
./dev/start-beat
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual commands
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Show manual setup and run steps</summary>
|
|
||||||
|
|
||||||
These commands assume you start from the repository root.
|
|
||||||
|
|
||||||
1. Start the docker-compose stack.
|
|
||||||
|
|
||||||
The backend requires middleware, including PostgreSQL, Redis, and Weaviate, which can be started together using `docker-compose`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp docker/middleware.env.example docker/middleware.env
|
|
||||||
# Use mysql or another vector database profile if you are not using postgres/weaviate.
|
|
||||||
docker compose -f docker/docker-compose.middleware.yaml --profile postgresql --profile weaviate -p dify up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Copy env files.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp api/.env.example api/.env
|
|
||||||
cp web/.env.example web/.env.local
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Install UV if needed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install uv
|
|
||||||
# Or on macOS
|
|
||||||
brew install uv
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Install API dependencies.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd api
|
|
||||||
uv sync --group dev
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Install web dependencies.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd web
|
|
||||||
pnpm install
|
|
||||||
cd ..
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Start backend (runs migrations first, in a new terminal).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd api
|
|
||||||
uv run flask db upgrade
|
|
||||||
uv run flask run --host 0.0.0.0 --port=5001 --debug
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Start Dify [web](../web) service (in a new terminal).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd web
|
|
||||||
pnpm dev:inspect
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Set up your application by visiting `http://localhost:3000`.
|
|
||||||
|
|
||||||
1. Optional: start the worker service (async tasks, in a new terminal).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd api
|
|
||||||
uv run celery -A app.celery worker -P threads -c 2 --loglevel INFO -Q api_token,dataset,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_executor,retention,workflow_based_app_execution
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Optional: start Celery Beat (scheduled tasks, in a new terminal).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd api
|
|
||||||
uv run celery -A app.celery beat
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Environment notes
|
### Environment notes
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
@ -177,5 +99,13 @@ These commands assume you start from the repository root.
|
|||||||
./dev/reformat # Run all formatters and linters
|
./dev/reformat # Run all formatters and linters
|
||||||
uv run ruff check --fix ./ # Fix linting issues
|
uv run ruff check --fix ./ # Fix linting issues
|
||||||
uv run ruff format ./ # Format code
|
uv run ruff format ./ # Format code
|
||||||
uv run basedpyright . # Type checking
|
uv run pyrefly check # Type checking
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Generate TS stub
|
||||||
|
|
||||||
|
```
|
||||||
|
uv run dev/generate_swagger_specs.py --output-dir openapi
|
||||||
|
```
|
||||||
|
|
||||||
|
use https://jsontotable.org/openapi-to-typescript to convert to typescript
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Application configuration definitions, including file access settings.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- File access settings drive signed URL expiration and base URLs.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Config parsing tests under tests/unit_tests/configs.
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- Registers file-related API namespaces and routes for files service.
|
|
||||||
- Includes app-assets and sandbox archive proxy controllers.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- files_ns must include all file controller modules to register routes.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Coverage via controller unit tests and route registration smoke checks.
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- App assets download proxy endpoint (signed URL verification, stream from storage).
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Validates AssetPath fields (UUIDs, asset_type allowlist).
|
|
||||||
- Verifies tenant-scoped signature and expiration before reading storage.
|
|
||||||
- URL uses expires_at/nonce/sign query params.
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- Missing files return NotFound.
|
|
||||||
- Invalid signature or expired link returns Forbidden.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Verify signature validation and invalid/expired cases.
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- App assets upload proxy endpoint (signed URL verification, upload to storage).
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Validates AssetPath fields (UUIDs, asset_type allowlist).
|
|
||||||
- Verifies tenant-scoped signature and expiration before writing storage.
|
|
||||||
- URL uses expires_at/nonce/sign query params.
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- Invalid signature or expired link returns Forbidden.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Verify signature validation and invalid/expired cases.
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- Sandbox archive upload/download proxy endpoints (signed URL verification, stream to storage).
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Validates tenant_id and sandbox_id UUIDs.
|
|
||||||
- Verifies tenant-scoped signature and expiration before storage access.
|
|
||||||
- URL uses expires_at/nonce/sign query params.
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- Missing archive returns NotFound.
|
|
||||||
- Invalid signature or expired link returns Forbidden.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Add unit tests for signature validation if needed.
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Collects file assets and emits FileAsset entries with storage keys.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Storage keys are derived via AppAssetStorage for draft files.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Covered by asset build pipeline tests.
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Builds skill artifacts from markdown assets and uploads resolved outputs.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Reads draft asset content via AppAssetStorage refs.
|
|
||||||
- Writes resolved artifacts via AppAssetStorage refs.
|
|
||||||
- FileAsset storage keys are derived via AppAssetStorage.
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- Missing or invalid JSON content yields empty skill content/metadata.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Build pipeline unit tests covering compile/upload paths.
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Converts AppAssetFileTree to FileAsset items for packaging.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Storage keys for assets are derived via AppAssetStorage.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Used in packaging/service tests for asset bundles.
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
# Zip Packager Notes
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
- Builds a ZIP archive of asset contents stored via the configured storage backend.
|
|
||||||
|
|
||||||
## Key Decisions
|
|
||||||
- Packaging writes assets into an in-memory zip buffer returned as bytes.
|
|
||||||
- Asset fetch + zip writing are executed via a thread pool with a lock guarding `ZipFile` writes.
|
|
||||||
|
|
||||||
## Edge Cases
|
|
||||||
- ZIP writes are serialized by the lock; storage reads still run in parallel.
|
|
||||||
|
|
||||||
## Tests/Verification
|
|
||||||
- None yet.
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Builds AssetItem entries for asset trees using AssetPath-derived storage keys.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Uses AssetPath to compute draft storage keys.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Covered by asset parsing and packaging tests.
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- Defines AssetPath facade + typed asset path classes for app-asset storage access.
|
|
||||||
- Maps asset paths to storage keys and generates presigned or signed-proxy URLs.
|
|
||||||
- Signs proxy URLs using tenant private keys and enforces expiration.
|
|
||||||
- Exposes app_asset_storage singleton for reuse.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- AssetPathBase fields (tenant_id/app_id/resource_id/node_id) must be UUIDs.
|
|
||||||
- AssetPath.from_components enforces valid types and resolved node_id presence.
|
|
||||||
- Storage keys are derived internally via AssetPathBase.get_storage_key; callers never supply raw paths.
|
|
||||||
- AppAssetStorage.storage returns the cached presign wrapper (not the raw storage).
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- Storage backends without presign support must fall back to signed proxy URLs.
|
|
||||||
- Signed proxy verification enforces expiration and tenant-scoped signing keys.
|
|
||||||
- Upload URLs also fall back to signed proxy endpoints when presign is unsupported.
|
|
||||||
- load_or_none treats SilentStorage "File Not Found" bytes as missing.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Unit tests for ref validation, storage key mapping, and signed URL verification.
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Extracts asset files from a zip and persists them into app asset storage.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Rejects path traversal/absolute/backslash paths.
|
|
||||||
- Saves extracted files via AppAssetStorage draft refs.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Zip security edge cases and tree construction tests.
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Downloads published app asset zip into sandbox and extracts it.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Uses AppAssetStorage to generate download URLs for build zips (internal URL).
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Sandbox initialization integration tests.
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Downloads draft/resolved assets into sandbox for draft execution.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Uses AppAssetStorage to generate download URLs for draft/resolved refs (internal URL).
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- No nodes -> returns early.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Sandbox draft initialization tests.
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- Sandbox lifecycle wrapper (ready/cancel/fail signals, mount/unmount, release).
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- wait_ready raises with the original initialization error as the cause.
|
|
||||||
- release always attempts unmount and environment release, logging failures.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Covered by sandbox lifecycle/unit tests and workflow execution error handling.
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- Sandbox security helper modules.
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- Generates and verifies signed URLs for sandbox archive upload/download.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- tenant_id and sandbox_id must be UUIDs.
|
|
||||||
- Signatures are tenant-scoped and include operation, expiry, and nonce.
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- Missing tenant private key raises ValueError.
|
|
||||||
- Expired or tampered signatures are rejected.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Add unit tests if sandbox archive signature behavior expands.
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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.
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
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.
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
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.
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
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.
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
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 os
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, cast
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
@ -10,12 +10,25 @@ 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
|
||||||
@ -38,13 +51,12 @@ else:
|
|||||||
|
|
||||||
socketio_app, flask_app = create_app()
|
socketio_app, flask_app = create_app()
|
||||||
app = flask_app
|
app = flask_app
|
||||||
celery = cast("Celery", flask_app.extensions["celery"])
|
celery = cast("Celery", 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]
|
||||||
|
|
||||||
host = os.environ.get("HOST", "0.0.0.0")
|
log_startup_banner(HOST, PORT)
|
||||||
port = int(os.environ.get("PORT", 5001))
|
server = pywsgi.WSGIServer((HOST, PORT), socketio_app, handler_class=WebSocketHandler)
|
||||||
server = pywsgi.WSGIServer((host, port), socketio_app, handler_class=WebSocketHandler)
|
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
|
|||||||
@ -117,7 +117,7 @@ def create_flask_app_with_configs() -> DifyApp:
|
|||||||
logger.warning("Failed to add trace headers to response", exc_info=True)
|
logger.warning("Failed to add trace headers to response", exc_info=True)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Capture the decorator's return value to avoid pyright reportUnusedFunction
|
# Capture the decorator return values so static checkers do not treat the hooks as unused.
|
||||||
_ = before_request
|
_ = before_request
|
||||||
_ = add_trace_headers
|
_ = add_trace_headers
|
||||||
|
|
||||||
@ -149,6 +149,7 @@ 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,
|
||||||
@ -180,7 +181,6 @@ 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,6 +188,7 @@ 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,
|
||||||
@ -199,6 +200,7 @@ 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,
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user