mirror of
https://github.com/penpot/penpot.git
synced 2026-08-25 06:08:47 +00:00
Compare commits
543 Commits
2.17.0-RC6
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64a52d6b04 | ||
|
|
dd6b521bc7 | ||
|
|
47d599fe34 | ||
|
|
8aefa2ddfd | ||
|
|
0cacf9bd99 | ||
|
|
7c85837290 | ||
|
|
dd4a163217 | ||
|
|
9fa07e7468 | ||
|
|
6d4a6f6a9a | ||
|
|
77971740e6 | ||
|
|
2318866f8d | ||
|
|
4cb9f951d2 | ||
|
|
5dab689a6e | ||
|
|
689d506788 | ||
|
|
ca72213cbb | ||
|
|
f29a94058a | ||
|
|
9f6878d118 | ||
|
|
2dcf1a8a0a | ||
|
|
209aea8365 | ||
|
|
c200a4d777 | ||
|
|
ed588d4500 | ||
|
|
a91c796b0e | ||
|
|
c378ec9218 | ||
|
|
4da6499197 | ||
|
|
aa3bc1ae98 | ||
|
|
60d87a6342 | ||
|
|
fda6d56139 | ||
|
|
1886697458 | ||
|
|
5080a90f76 | ||
|
|
4d90fe9126 | ||
|
|
54aaebee1e | ||
|
|
8da13b5fa1 | ||
|
|
ddc98bdd47 | ||
|
|
d826c7ac13 | ||
|
|
ddd32670b3 | ||
|
|
4339d8d244 | ||
|
|
b6c4cb48d7 | ||
|
|
df664fe96b | ||
|
|
7061ecae0a | ||
|
|
4ac14cfd08 | ||
|
|
1671cc4fcc | ||
|
|
e72c1869eb | ||
|
|
3be07ccced | ||
|
|
73c0668877 | ||
|
|
367e4d534c | ||
|
|
aa5545c258 | ||
|
|
3f09f161ae | ||
|
|
d3bee4ba9d | ||
|
|
162a381aed | ||
|
|
296dd748bd | ||
|
|
5b4a5776cb | ||
|
|
904570f970 | ||
|
|
d745dc4a3c | ||
|
|
b4bc3dfe6a | ||
|
|
c72bb331ef | ||
|
|
7f2dc66e86 | ||
|
|
9311737f66 | ||
|
|
7ac61e0597 | ||
|
|
8acb92b782 | ||
|
|
fb9f92ae6a | ||
|
|
c797656d17 | ||
|
|
f96d850049 | ||
|
|
ed04d509ed | ||
|
|
57c9c3f6a4 | ||
|
|
29dbf9ab12 | ||
|
|
509f5395cb | ||
|
|
4ecd8ffb89 | ||
|
|
0797d7235a | ||
|
|
5efd9cc3c5 | ||
|
|
e219ce20eb | ||
|
|
9e97477a98 | ||
|
|
c688cba8d8 | ||
|
|
68e1db984d | ||
|
|
3033da4409 | ||
|
|
aecfee0f02 | ||
|
|
59ef07633a | ||
|
|
ba235f46c9 | ||
|
|
e56c801820 | ||
|
|
a3bc4b0e3a | ||
|
|
6269fa7a3f | ||
|
|
350dc14632 | ||
|
|
136052c15e | ||
|
|
c7f036bed0 | ||
|
|
3db7548c19 | ||
|
|
cb57fd9dfa | ||
|
|
9528400c6e | ||
|
|
f7fc869e52 | ||
|
|
be83656d55 | ||
|
|
1c14c854ae | ||
|
|
af1537d071 | ||
|
|
ef26231b8f | ||
|
|
3b9e0782e4 | ||
|
|
93f02ea0b4 | ||
|
|
6d49fb2be0 | ||
|
|
201b51e8c5 | ||
|
|
fee416d275 | ||
|
|
986ee60cad | ||
|
|
e5c80edbf3 | ||
|
|
732162e720 | ||
|
|
be9df28b00 | ||
|
|
868340dfba | ||
|
|
9f17aa6216 | ||
|
|
290b14167a | ||
|
|
985d219810 | ||
|
|
044d7ac15f | ||
|
|
53985dc630 | ||
|
|
02c31e7348 | ||
|
|
4a1d6e6d57 | ||
|
|
69ef7e86cd | ||
|
|
d7daefafe2 | ||
|
|
c4d1a1bc94 | ||
|
|
0de47302a6 | ||
|
|
1e6d438257 | ||
|
|
d4294bbf1e | ||
|
|
83efa28b12 | ||
|
|
16e52b0494 | ||
|
|
0fd2a9d26f | ||
|
|
5d2cb22966 | ||
|
|
900a7ef498 | ||
|
|
86c563f11f | ||
|
|
d63d6370c0 | ||
|
|
b9c92496f1 | ||
|
|
fcd33340b3 | ||
|
|
e01b36b841 | ||
|
|
b5bec4f983 | ||
|
|
2f04fcddbf | ||
|
|
e2d429d283 | ||
|
|
30bc2a4bc3 | ||
|
|
bf9825fcfe | ||
|
|
a131e40a6d | ||
|
|
5571c53502 | ||
|
|
6951876c13 | ||
|
|
399b00b86d | ||
|
|
43b12bc4b9 | ||
|
|
e1c51442cd | ||
|
|
bc9319eac5 | ||
|
|
5359ff04cf | ||
|
|
88697794ce | ||
|
|
9875db2f82 | ||
|
|
1548748aed | ||
|
|
0702363b5c | ||
|
|
688c69b478 | ||
|
|
38b990ef90 | ||
|
|
a76401596e | ||
|
|
de8d8ca401 | ||
|
|
314a2a245f | ||
|
|
614d619173 | ||
|
|
229d24e8f2 | ||
|
|
fdf1684565 | ||
|
|
2392015c63 | ||
|
|
11fc090bc4 | ||
|
|
81e44afbe3 | ||
|
|
10a2c19f92 | ||
|
|
495e9f059e | ||
|
|
4b413299c2 | ||
|
|
31c9ab4701 | ||
|
|
8b64b0f84f | ||
|
|
a60b648c6c | ||
|
|
649f4bebef | ||
|
|
b6656ee8dd | ||
|
|
86aaf642b6 | ||
|
|
c4dd04353f | ||
|
|
0ac711aa68 | ||
|
|
bf62e59f73 | ||
|
|
5906312dff | ||
|
|
25066c2f46 | ||
|
|
3d176d5390 | ||
|
|
0481408531 | ||
|
|
689d3a1be2 | ||
|
|
fb07273897 | ||
|
|
9242556da6 | ||
|
|
4f7bb94bb1 | ||
|
|
5b26913cd3 | ||
|
|
36e76da26c | ||
|
|
49276886f3 | ||
|
|
35bdcde183 | ||
|
|
a2968defbe | ||
|
|
6df045b194 | ||
|
|
1b26b69b25 | ||
|
|
6628f0a134 | ||
|
|
6f2bfb617c | ||
|
|
636bc22cc4 | ||
|
|
aeedb96260 | ||
|
|
3e59754a25 | ||
|
|
c16b7919f9 | ||
|
|
34702fd46b | ||
|
|
83a3d099f6 | ||
|
|
8e713df5f0 | ||
|
|
3fba272848 | ||
|
|
648c8e2152 | ||
|
|
7ae57a035f | ||
|
|
23ea2bbad6 | ||
|
|
14a6ea5c52 | ||
|
|
ca29f734c7 | ||
|
|
b507a6b667 | ||
|
|
3865e29b65 | ||
|
|
43e05c38bf | ||
|
|
0811b1cda6 | ||
|
|
edbe9f8215 | ||
|
|
6e843faba3 | ||
|
|
319a2185c9 | ||
|
|
1136e5eda5 | ||
|
|
49119e0339 | ||
|
|
c6c8a38544 | ||
|
|
0fed63eeb3 | ||
|
|
79da4d274d | ||
|
|
2f535c3f3f | ||
|
|
c320cecf15 | ||
|
|
141cf7f79f | ||
|
|
767f90282c | ||
|
|
eacdca3c0a | ||
|
|
b72536d312 | ||
|
|
d835baefec | ||
|
|
f023c09018 | ||
|
|
32520e66e5 | ||
|
|
5ad0ff752e | ||
|
|
3fc5360df5 | ||
|
|
b907bc4cd3 | ||
|
|
73519778ae | ||
|
|
97a833f1e7 | ||
|
|
e741313add | ||
|
|
896aef8c1b | ||
|
|
328fa0559a | ||
|
|
863671135d | ||
|
|
657b5bd94e | ||
|
|
5ea3a2efb8 | ||
|
|
94cfd6e60d | ||
|
|
600c573ccd | ||
|
|
4c388f6d8b | ||
|
|
4d07510581 | ||
|
|
5e8a2b0a03 | ||
|
|
588222d409 | ||
|
|
0139c0f0e3 | ||
|
|
1b69d8c6a2 | ||
|
|
d7b55a245a | ||
|
|
fdeff910fe | ||
|
|
3fa21180fd | ||
|
|
ecaf63b204 | ||
|
|
1074cb376d | ||
|
|
0aff047301 | ||
|
|
02b0f18a83 | ||
|
|
13b2954cbf | ||
|
|
0703a3fcc1 | ||
|
|
b294aecb7c | ||
|
|
620c26b304 | ||
|
|
1ac000bc27 | ||
|
|
9042bd5129 | ||
|
|
905c230e17 | ||
|
|
552cda9aae | ||
|
|
89029060e0 | ||
|
|
07efd8fb99 | ||
|
|
31158c319a | ||
|
|
ab1c70115b | ||
|
|
9bb5861322 | ||
|
|
d04cbf175e | ||
|
|
36553a7c6f | ||
|
|
ff32f104b9 | ||
|
|
1744d07731 | ||
|
|
764b62906b | ||
|
|
94f51afb20 | ||
|
|
a2231a8bf9 | ||
|
|
f5b17a5c75 | ||
|
|
e952d9c70a | ||
|
|
3fc1e6aadd | ||
|
|
0b5db57def | ||
|
|
7fa6631005 | ||
|
|
9b0be1c750 | ||
|
|
fe85a5717e | ||
|
|
370e91c3e3 | ||
|
|
31ccfa546f | ||
|
|
91d4b16171 | ||
|
|
c8867f8c3a | ||
|
|
30943f1074 | ||
|
|
b4659df5b2 | ||
|
|
1ae9334064 | ||
|
|
014ec34249 | ||
|
|
e705b6e12c | ||
|
|
972353eccb | ||
|
|
ef593514f2 | ||
|
|
040080749b | ||
|
|
fadb3124a0 | ||
|
|
3eb50ef50e | ||
|
|
a722a33503 | ||
|
|
03f9393200 | ||
|
|
be5d9f9e9c | ||
|
|
2cb8813862 | ||
|
|
69649cd608 | ||
|
|
e5d588ca54 | ||
|
|
485cb8e5ec | ||
|
|
25618febcd | ||
|
|
5e5465a0fe | ||
|
|
0ed151d2ca | ||
|
|
adda7e6645 | ||
|
|
3e7fb07931 | ||
|
|
c8d1f5b397 | ||
|
|
056cd3d379 | ||
|
|
4cd34b22e2 | ||
|
|
e14e7bb616 | ||
|
|
66201ac437 | ||
|
|
70675919c7 | ||
|
|
9f92206a1c | ||
|
|
704cd40182 | ||
|
|
cb21f6401a | ||
|
|
0999bc31b2 | ||
|
|
343865cf27 | ||
|
|
e9603f00b0 | ||
|
|
bbc7e9bee9 | ||
|
|
65111195a9 | ||
|
|
6bbd3fa364 | ||
|
|
d4d22ff1a4 | ||
|
|
0c02121d5d | ||
|
|
7674229a5b | ||
|
|
af00860c13 | ||
|
|
4b994d20aa | ||
|
|
7eb188b077 | ||
|
|
535ccfb930 | ||
|
|
6c2b61e1ad | ||
|
|
458fa41036 | ||
|
|
6063a45c3c | ||
|
|
68dccd6ac2 | ||
|
|
678e206fe6 | ||
|
|
63e0c536f0 | ||
|
|
ff16b8bbef | ||
|
|
af120feb1f | ||
|
|
5a0cee44b1 | ||
|
|
5ce206f904 | ||
|
|
af51c897a8 | ||
|
|
44c15480f2 | ||
|
|
f60c82300a | ||
|
|
aad9bc8f65 | ||
|
|
e17f0ea7ee | ||
|
|
bb8f965cf0 | ||
|
|
a749763fb1 | ||
|
|
2f8d71e143 | ||
|
|
0b072b22b0 | ||
|
|
170d129a5a | ||
|
|
eda5aa76f0 | ||
|
|
b54c1f316a | ||
|
|
66bb6a11df | ||
|
|
62880cb58c | ||
|
|
8142f4949e | ||
|
|
c10c7b08aa | ||
|
|
56c3e9777e | ||
|
|
8b485b4a8b | ||
|
|
d94b139071 | ||
|
|
3610b81e4b | ||
|
|
5de06e8f77 | ||
|
|
caacd482af | ||
|
|
4f46700c94 | ||
|
|
c467d98c9e | ||
|
|
d5d6c61ba4 | ||
|
|
6e85ce5d7a | ||
|
|
f7c312021b | ||
|
|
3fa7943344 | ||
|
|
19bdd7081b | ||
|
|
ed85d1d1af | ||
|
|
d42f78b80e | ||
|
|
45405a018b | ||
|
|
8bf411c347 | ||
|
|
9b88d35664 | ||
|
|
e4d88b3ab4 | ||
|
|
66b978c01e | ||
|
|
52e5e0bec6 | ||
|
|
7430ffe718 | ||
|
|
09730d10d0 | ||
|
|
b3091399cd | ||
|
|
b6629c0034 | ||
|
|
c57f90d4a2 | ||
|
|
707cfac375 | ||
|
|
4900d4b24a | ||
|
|
702a435569 | ||
|
|
bac739717c | ||
|
|
4c222c469a | ||
|
|
b1ccb252fd | ||
|
|
4383cf183a | ||
|
|
eb1e0ad186 | ||
|
|
7b9896ab32 | ||
|
|
2523a72c32 | ||
|
|
e076443eca | ||
|
|
ef3511e519 | ||
|
|
f21bd45893 | ||
|
|
40ab48ea01 | ||
|
|
396d799c71 | ||
|
|
f20e4280fc | ||
|
|
2344ba22a6 | ||
|
|
018d840bab | ||
|
|
e1f976aa2f | ||
|
|
80c84a3331 | ||
|
|
9cb039070f | ||
|
|
9f029de578 | ||
|
|
3a0aca52c3 | ||
|
|
f9439d2942 | ||
|
|
e4cddd8536 | ||
|
|
72f3165341 | ||
|
|
f98b1ddb8e | ||
|
|
08e42687d1 | ||
|
|
d4e87ec59d | ||
|
|
b8e3089ae7 | ||
|
|
1c917951b6 | ||
|
|
f3bf24b4f6 | ||
|
|
73bfc0dc15 | ||
|
|
bdce5817ea | ||
|
|
4ecbac896f | ||
|
|
5dff551f31 | ||
|
|
fa996ab240 | ||
|
|
0ff73ae711 | ||
|
|
6c5d01283a | ||
|
|
64026fc1f6 | ||
|
|
2de08f00e8 | ||
|
|
4ea56e0b89 | ||
|
|
27392abd49 | ||
|
|
6c5618025d | ||
|
|
4dc59ede50 | ||
|
|
378d97df93 | ||
|
|
79a471a6f1 | ||
|
|
f2a9dd1a08 | ||
|
|
f06339fb87 | ||
|
|
c6de3ce47b | ||
|
|
a0b5c23f99 | ||
|
|
51c0129485 | ||
|
|
4c21923c4a | ||
|
|
5f35fdf217 | ||
|
|
5dadda0f2b | ||
|
|
5acc3aac99 | ||
|
|
8d98877ba7 | ||
|
|
826072c65d | ||
|
|
cddbd8e897 | ||
|
|
29ed79fcdc | ||
|
|
9aac607552 | ||
|
|
4ea83d31fc | ||
|
|
33d478b532 | ||
|
|
d9511db585 | ||
|
|
a4347451d0 | ||
|
|
7e36192034 | ||
|
|
5cc6477e3f | ||
|
|
17c344b8f5 | ||
|
|
001dba1bef | ||
|
|
c50ec233ae | ||
|
|
bdc078d5ea | ||
|
|
792d88dc4f | ||
|
|
2203482531 | ||
|
|
167aa7410f | ||
|
|
c119622ad9 | ||
|
|
95cfbf5f7c | ||
|
|
c11d3aaaa7 | ||
|
|
a006a12ab6 | ||
|
|
2c15dcdb84 | ||
|
|
176a813fb9 | ||
|
|
fd8cb957d1 | ||
|
|
89551b2415 | ||
|
|
3469867cf5 | ||
|
|
3ee5b50007 | ||
|
|
508569437a | ||
|
|
1311bafec4 | ||
|
|
f84a8687f6 | ||
|
|
ebfcb716ac | ||
|
|
80d688be93 | ||
|
|
54bef496f1 | ||
|
|
fb2ec4c617 | ||
|
|
a004f7cd84 | ||
|
|
5d933fd770 | ||
|
|
cc454de9ce | ||
|
|
0de8bce895 | ||
|
|
d4aa512247 | ||
|
|
0e25f7d8ad | ||
|
|
4eaefec43e | ||
|
|
f51db39b8d | ||
|
|
a58c0e1407 | ||
|
|
3e2826de9e | ||
|
|
ac31edab14 | ||
|
|
83d2edf256 | ||
|
|
a4ba5fc2e0 | ||
|
|
04f6549e82 | ||
|
|
0182f239ae | ||
|
|
2c4a9e0f82 | ||
|
|
841b47736b | ||
|
|
0387cdf7e4 | ||
|
|
2e32a8743e | ||
|
|
b82ab0c830 | ||
|
|
30e17eb076 | ||
|
|
40091924e2 | ||
|
|
b3585428bd | ||
|
|
6d458c80a1 | ||
|
|
64ba70e6f3 | ||
|
|
215de966ae | ||
|
|
51877c4bca | ||
|
|
782adffec8 | ||
|
|
c4e72fd7f9 | ||
|
|
8d5b16295f | ||
|
|
46f5346045 | ||
|
|
b3b3ea97db | ||
|
|
e6a49adfbc | ||
|
|
557a267176 | ||
|
|
f05fb70a8a | ||
|
|
2d4a24bf97 | ||
|
|
a9f3949abc | ||
|
|
785c5ffdfc | ||
|
|
3369abd988 | ||
|
|
5212e2202b | ||
|
|
6e61e3304b | ||
|
|
f8974288e7 | ||
|
|
d328cb4a9e | ||
|
|
14fb211733 | ||
|
|
ce1191d86f | ||
|
|
f996ef372d | ||
|
|
adbb5a8b5f | ||
|
|
2a5b6a69ad | ||
|
|
2eb9423963 | ||
|
|
15a336a249 | ||
|
|
28f3b8048a | ||
|
|
d58d816310 | ||
|
|
aedb7f9195 | ||
|
|
a6c7bd28e8 | ||
|
|
b9136e6fda | ||
|
|
3036ef473e | ||
|
|
673c87cf33 | ||
|
|
403e1ec604 | ||
|
|
0270c2a90f | ||
|
|
5ef8d35683 | ||
|
|
a530cf0dec | ||
|
|
d757f96633 | ||
|
|
8fa15f240f | ||
|
|
fa9012e55f | ||
|
|
06e6671813 | ||
|
|
1a88864cbb | ||
|
|
30be0cae71 | ||
|
|
cf4fb8d4a2 | ||
|
|
af83a0c7f7 | ||
|
|
1b3a3b4cdb | ||
|
|
121c76235f | ||
|
|
fc07a6467b | ||
|
|
46abf1c968 | ||
|
|
66c8ebf198 | ||
|
|
ea20291d2a | ||
|
|
0dbc2c54d6 | ||
|
|
4c5991514a | ||
|
|
a0d9603243 | ||
|
|
22cf4917d8 | ||
|
|
4bbf9a6617 | ||
|
|
f2bf5a3111 | ||
|
|
7c19ace0f0 | ||
|
|
e8e0d68019 | ||
|
|
5f8d9740d6 |
@ -88,6 +88,9 @@
|
|||||||
:dynamic-var-not-earmuffed
|
:dynamic-var-not-earmuffed
|
||||||
{:level :off}
|
{:level :off}
|
||||||
|
|
||||||
|
:type-mismatch
|
||||||
|
{:level :off}
|
||||||
|
|
||||||
:used-underscored-binding
|
:used-underscored-binding
|
||||||
{:level :warning}
|
{:level :warning}
|
||||||
|
|
||||||
|
|||||||
3
.env.example
Normal file
3
.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Penpot API configuration for error-reports CLI tool
|
||||||
|
PENPOT_API_URI=http://localhost:3450
|
||||||
|
PENPOT_ACCESS_TOKEN=your-access-token-here
|
||||||
41
.github/scripts/playwright-summary.jq
vendored
Normal file
41
.github/scripts/playwright-summary.jq
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
def specs: [.. | objects | select(has("tests") and has("file"))];
|
||||||
|
def dur: [.tests[].results[]?.duration // 0] | add;
|
||||||
|
|
||||||
|
specs as $s
|
||||||
|
| ($s | map(select(any(.tests[]; .status == "unexpected")))) as $failed
|
||||||
|
| ($s | map(select(any(.tests[]; .status == "flaky")))) as $flaky
|
||||||
|
| ($s | map(select(any(.tests[]; .status == "skipped")))) as $skipped
|
||||||
|
| ($s | length) as $total
|
||||||
|
| ($s | map(dur) | add // 0 | . / 1000 | floor) as $cpu
|
||||||
|
| (if ($failed | length) > 0 then "❌"
|
||||||
|
elif ($flaky | length) > 0 then "⚠️"
|
||||||
|
else "✅" end) as $icon
|
||||||
|
|
||||||
|
| "## \($icon) Integration tests\n\n"
|
||||||
|
+ "| Total | Passed | Flaky | Failed | Skipped | Test time |\n"
|
||||||
|
+ "|---|---|---|---|---|---|\n"
|
||||||
|
+ "| \($total) | \($total - ($failed|length) - ($flaky|length) - ($skipped|length)) "
|
||||||
|
+ "| \($flaky|length) | \($failed|length) | \($skipped|length) | \($cpu / 60 | floor)m |\n"
|
||||||
|
|
||||||
|
+ (if ($failed | length) > 0 then
|
||||||
|
"\n### Failed\n\n"
|
||||||
|
+ ($failed | map("- `\(.file):\(.line)` — \(.title)") | join("\n")) + "\n"
|
||||||
|
else "" end)
|
||||||
|
|
||||||
|
+ (if ($flaky | length) > 0 then
|
||||||
|
"\n### Flaky (passed on retry)\n\n"
|
||||||
|
+ ($flaky
|
||||||
|
| map({ t: "`\(.file):\(.line)` — \(.title)",
|
||||||
|
r: ([.tests[].results[]? | select(.status == "failed")] | length) })
|
||||||
|
| sort_by(-.r)
|
||||||
|
| map("- \(.t) _(\(.r) \(if .r == 1 then "retry" else "retries" end))_")
|
||||||
|
| join("\n")) + "\n"
|
||||||
|
else "" end)
|
||||||
|
|
||||||
|
+ (if $total > 0 then
|
||||||
|
"\n<details><summary>Slowest specs</summary>\n\n"
|
||||||
|
+ ($s | map({ t: "`\(.file)` — \(.title)", d: (dur / 1000 | floor) })
|
||||||
|
| sort_by(-.d) | .[0:5]
|
||||||
|
| map("- \(.t) — \(.d)s") | join("\n"))
|
||||||
|
+ "\n\n</details>\n"
|
||||||
|
else "" end)
|
||||||
2
.github/workflows/auto-label.yml
vendored
2
.github/workflows/auto-label.yml
vendored
@ -3,7 +3,7 @@ name: Auto Label and Add to Project
|
|||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened]
|
types: [opened]
|
||||||
pull_request:
|
pull_request_target:
|
||||||
types: [opened]
|
types: [opened]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
106
.github/workflows/build-bundle.yml
vendored
106
.github/workflows/build-bundle.yml
vendored
@ -9,16 +9,6 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
default: 'develop'
|
default: 'develop'
|
||||||
build_wasm:
|
|
||||||
description: 'BUILD_WASM. Valid values: yes, no'
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
default: 'yes'
|
|
||||||
build_storybook:
|
|
||||||
description: 'BUILD_STORYBOOK. Valid values: yes, no'
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
default: 'yes'
|
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
gh_ref:
|
gh_ref:
|
||||||
@ -26,29 +16,21 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
default: 'develop'
|
default: 'develop'
|
||||||
build_wasm:
|
|
||||||
description: 'BUILD_WASM. Valid values: yes, no'
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
default: 'yes'
|
|
||||||
build_storybook:
|
|
||||||
description: 'BUILD_STORYBOOK. Valid values: yes, no'
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
default: 'yes'
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
|
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-bundle:
|
# ── 1. Decide whether there is anything to build ───────────────────────
|
||||||
name: Build and Upload Penpot Bundle
|
check:
|
||||||
runs-on: penpot-runner-01
|
name: Check current bundle
|
||||||
env:
|
runs-on: penpot-standar-runner
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
timeout-minutes: 10
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
outputs:
|
||||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
gh_ref: ${{ steps.vars.outputs.gh_ref }}
|
||||||
|
bundle_version: ${{ steps.vars.outputs.bundle_version }}
|
||||||
|
exists: ${{ steps.check.outputs.exists }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@ -63,10 +45,52 @@ jobs:
|
|||||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||||
echo "bundle_version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
|
echo "bundle_version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# The uploaded zip carries its version as S3 metadata. If the
|
||||||
|
# existing object was already built from this same commit, the
|
||||||
|
# whole build job is skipped.
|
||||||
|
- name: Check if this bundle is already built
|
||||||
|
id: check
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||||
|
run: |
|
||||||
|
EXISTING_VERSION=$(aws s3api head-object \
|
||||||
|
--bucket ${{ secrets.S3_BUCKET }} \
|
||||||
|
--key "penpot-${{ steps.vars.outputs.gh_ref }}.zip" \
|
||||||
|
--query 'Metadata."bundle-version"' \
|
||||||
|
--output text 2>/dev/null || echo "none")
|
||||||
|
|
||||||
|
if [ "$EXISTING_VERSION" = "${{ steps.vars.outputs.bundle_version }}" ]; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
{
|
||||||
|
echo "### ⏭️ Bundle build skipped"
|
||||||
|
echo ""
|
||||||
|
echo "The bundle in S3 was already built from \`${{ steps.vars.outputs.bundle_version }}\`."
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 2. Build and upload, only when needed ──────────────────────────────
|
||||||
|
build:
|
||||||
|
name: Build and Upload Penpot Bundle
|
||||||
|
runs-on: penpot-standar-runner
|
||||||
|
timeout-minutes: 90
|
||||||
|
needs: check
|
||||||
|
if: needs.check.outputs.exists == 'false'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ inputs.gh_ref }}
|
||||||
|
|
||||||
- name: Build bundle
|
- name: Build bundle
|
||||||
env:
|
env:
|
||||||
BUILD_WASM: ${{ inputs.build_wasm }}
|
BUILD_WASM: 'yes'
|
||||||
BUILD_STORYBOOK: ${{ inputs.build_storybook }}
|
BUILD_STORYBOOK: 'yes'
|
||||||
run: ./manage.sh build-bundle
|
run: ./manage.sh build-bundle
|
||||||
|
|
||||||
- name: Prepare directories for zipping
|
- name: Prepare directories for zipping
|
||||||
@ -80,18 +104,32 @@ jobs:
|
|||||||
zip -r zips/penpot.zip penpot
|
zip -r zips/penpot.zip penpot
|
||||||
|
|
||||||
- name: Upload Penpot bundle to S3
|
- name: Upload Penpot bundle to S3
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||||
run: |
|
run: |
|
||||||
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip --metadata bundle-version=${{ steps.vars.outputs.bundle_version }}
|
aws s3 cp zips/penpot.zip \
|
||||||
|
s3://${{ secrets.S3_BUCKET }}/penpot-${{ needs.check.outputs.gh_ref }}.zip \
|
||||||
|
--metadata bundle-version=${{ needs.check.outputs.bundle_version }}
|
||||||
|
|
||||||
- name: Notify Mattermost
|
# ── 3. Single failure notification for the whole workflow ─────────────
|
||||||
|
notify:
|
||||||
|
name: Notify failure
|
||||||
|
runs-on: penpot-standar-runner
|
||||||
|
timeout-minutes: 5
|
||||||
|
needs: [check, build]
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: mattermost/action-mattermost-notify@master
|
|
||||||
|
steps:
|
||||||
|
- name: Notify Mattermost
|
||||||
|
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||||
with:
|
with:
|
||||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||||
TEXT: |
|
TEXT: |
|
||||||
❌ 📦 *[PENPOT] Error building penpot bundles.*
|
❌ 📦 *[PENPOT] Error building penpot bundles.*
|
||||||
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
|
📄 Triggered from ref: `${{ needs.check.outputs.gh_ref || inputs.gh_ref }}`
|
||||||
Bundle version: `${{ steps.vars.outputs.bundle_version }}`
|
Bundle version: `${{ needs.check.outputs.bundle_version || 'n/a' }}`
|
||||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
@infra
|
@infra
|
||||||
|
|||||||
8
.github/workflows/build-develop.yml
vendored
8
.github/workflows/build-develop.yml
vendored
@ -11,8 +11,6 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
gh_ref: "develop"
|
gh_ref: "develop"
|
||||||
build_wasm: "yes"
|
|
||||||
build_storybook: "yes"
|
|
||||||
|
|
||||||
build-docker:
|
build-docker:
|
||||||
needs: build-bundle
|
needs: build-bundle
|
||||||
@ -20,3 +18,9 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
gh_ref: "develop"
|
gh_ref: "develop"
|
||||||
|
|
||||||
|
build-docker-admin-console:
|
||||||
|
uses: ./.github/workflows/build-docker-admin-console.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
gh_ref: "develop"
|
||||||
|
|||||||
91
.github/workflows/build-docker-admin-console.yml
vendored
Normal file
91
.github/workflows/build-docker-admin-console.yml
vendored
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
name: Admin Console Docker Builder
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
gh_ref:
|
||||||
|
description: 'Name of the branch or ref to build in penpot-nitrate'
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
default: 'develop'
|
||||||
|
dispatch_ref:
|
||||||
|
description: 'Branch of penpot-nitrate from which the workflow definition is read'
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: 'develop'
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
gh_ref:
|
||||||
|
description: 'Name of the branch or ref to build in penpot-nitrate'
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
dispatch_ref:
|
||||||
|
description: 'Branch of penpot-nitrate from which the workflow definition is read'
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: 'develop'
|
||||||
|
secrets:
|
||||||
|
ORG_WORKFLOW_TOKEN:
|
||||||
|
description: 'Token with Actions write access on penpot-nitrate'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-nitrate-docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.ORG_WORKFLOW_TOKEN }}
|
||||||
|
REPO: penpot/penpot-nitrate
|
||||||
|
WORKFLOW: build-docker-admin-console.yml
|
||||||
|
GH_REF: ${{ inputs.gh_ref }}
|
||||||
|
DISPATCH_REF: ${{ inputs.dispatch_ref }}
|
||||||
|
steps:
|
||||||
|
- name: Trigger nitrate docker build
|
||||||
|
id: dispatch
|
||||||
|
run: |
|
||||||
|
DISTINCT_ID="${{ github.run_id }}-${{ github.run_attempt }}"
|
||||||
|
CALLER_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
|
|
||||||
|
gh workflow run "$WORKFLOW" --repo "$REPO" --ref "$DISPATCH_REF" \
|
||||||
|
-f gh_ref="$GH_REF" \
|
||||||
|
-f caller_run_id="$DISTINCT_ID" \
|
||||||
|
-f caller_run_url="$CALLER_URL"
|
||||||
|
|
||||||
|
# Locate the dispatched run using the correlation id embedded in its run-name
|
||||||
|
RUN_ID=""
|
||||||
|
for i in $(seq 1 24); do
|
||||||
|
sleep 5
|
||||||
|
RUN_ID=$(gh run list --repo "$REPO" --workflow "$WORKFLOW" \
|
||||||
|
--limit 10 --json databaseId,displayTitle \
|
||||||
|
--jq ".[] | select(.displayTitle | contains(\"$DISTINCT_ID\")) | .databaseId" \
|
||||||
|
| head -n1)
|
||||||
|
[ -n "$RUN_ID" ] && break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$RUN_ID" ]; then
|
||||||
|
echo "::error::Could not locate the dispatched run in $REPO"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN_URL="https://github.com/$REPO/actions/runs/$RUN_ID"
|
||||||
|
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "run_url=$RUN_URL" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::notice title=Nitrate docker build::$RUN_URL"
|
||||||
|
|
||||||
|
- name: Wait for nitrate docker build
|
||||||
|
run: |
|
||||||
|
gh run watch "${{ steps.dispatch.outputs.run_id }}" \
|
||||||
|
--repo "$REPO" \
|
||||||
|
--interval 30 \
|
||||||
|
--exit-status
|
||||||
|
|
||||||
|
- name: Report result
|
||||||
|
if: always() && steps.dispatch.outputs.run_id != ''
|
||||||
|
run: |
|
||||||
|
CONCLUSION=$(gh run view "${{ steps.dispatch.outputs.run_id }}" \
|
||||||
|
--repo "$REPO" --json conclusion --jq '.conclusion')
|
||||||
|
{
|
||||||
|
echo "### 🐳 Nitrate docker build"
|
||||||
|
echo ""
|
||||||
|
echo "- Result: \`${CONCLUSION:-in_progress}\`"
|
||||||
|
echo "- Run: ${{ steps.dispatch.outputs.run_url }}"
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
15
.github/workflows/build-docker-devenv.yml
vendored
15
.github/workflows/build-docker-devenv.yml
vendored
@ -6,7 +6,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
name: Build and push DevEnv Docker image
|
name: Build and push DevEnv Docker image
|
||||||
runs-on: penpot-runner-02
|
runs-on: penpot-extended-runner
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set common environment variables
|
- name: Set common environment variables
|
||||||
@ -20,12 +20,19 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Login to Docker Registry
|
- name: Login to Docker Registry (push destination)
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to Docker Hardened Images registry (base image pull)
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: dhi.io
|
||||||
|
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Build and push DevEnv Docker image
|
- name: Build and push DevEnv Docker image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
env:
|
env:
|
||||||
@ -35,12 +42,14 @@ jobs:
|
|||||||
file: ./docker/devenv/Dockerfile
|
file: ./docker/devenv/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
|
provenance: mode=max
|
||||||
|
sbom: true
|
||||||
tags: ${{ env.DOCKER_IMAGE }}:latest
|
tags: ${{ env.DOCKER_IMAGE }}:latest
|
||||||
cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
|
cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
|
||||||
cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
||||||
|
|
||||||
- name: Notify Mattermost
|
- name: Notify Mattermost
|
||||||
uses: mattermost/action-mattermost-notify@master
|
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||||
with:
|
with:
|
||||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||||
|
|||||||
325
.github/workflows/build-docker.yml
vendored
325
.github/workflows/build-docker.yml
vendored
@ -20,55 +20,117 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
|
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
ALL_IMAGES: backend frontend exporter storybook mcp
|
||||||
|
# All runner instances live on the same server, so the bundle is
|
||||||
|
# downloaded from S3 once and shared between build jobs through this
|
||||||
|
# host-local directory. Each build job falls back to S3 if the file is
|
||||||
|
# missing (e.g. if runners ever move to separate machines).
|
||||||
|
BUNDLE_CACHE: /var/tmp/penpot-bundle-cache
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
# ── 1. Resolve the build key and check the whole set at once ───────────
|
||||||
name: Build and Push Penpot Docker Images
|
prepare:
|
||||||
runs-on: penpot-runner-02
|
name: Prepare
|
||||||
|
runs-on: penpot-extended-runner
|
||||||
|
timeout-minutes: 15
|
||||||
|
outputs:
|
||||||
|
gh_ref: ${{ steps.vars.outputs.gh_ref }}
|
||||||
|
bundle_version: ${{ steps.vars.outputs.bundle_version }}
|
||||||
|
build_key: ${{ steps.vars.outputs.build_key }}
|
||||||
|
exists: ${{ steps.check.outputs.exists }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.gh_ref }}
|
||||||
|
|
||||||
|
- name: Extract some useful variables
|
||||||
|
id: vars
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||||
|
run: |
|
||||||
|
GH_REF="${{ inputs.gh_ref || github.ref_name }}"
|
||||||
|
echo "gh_ref=$GH_REF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
BUNDLE_VERSION=$(aws s3api head-object \
|
||||||
|
--bucket ${{ secrets.S3_BUCKET }} \
|
||||||
|
--key "penpot-$GH_REF.zip" \
|
||||||
|
--query 'Metadata."bundle-version"' \
|
||||||
|
--output text)
|
||||||
|
echo "bundle_version=$BUNDLE_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Image content = bundle + docker build context, so the build key
|
||||||
|
# combines both.
|
||||||
|
CTX_HASH=$(git rev-parse "HEAD:docker/images" | cut -c1-12)
|
||||||
|
echo "build_key=${BUNDLE_VERSION}-${CTX_HASH}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# The image set is a single block, so a single set-level check is
|
||||||
|
# enough: `promote` drops a marker object in S3 only after every
|
||||||
|
# image was built AND every branch tag was moved. Marker present
|
||||||
|
# means there is nothing at all to do for this build key.
|
||||||
|
- name: Check if this image set is already built
|
||||||
|
id: check
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||||
|
run: |
|
||||||
|
if aws s3api head-object \
|
||||||
|
--bucket ${{ secrets.S3_BUCKET }} \
|
||||||
|
--key "markers/images-${{ steps.vars.outputs.build_key }}" \
|
||||||
|
> /dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
{
|
||||||
|
echo "### ⏭️ Image set build skipped"
|
||||||
|
echo ""
|
||||||
|
echo "The whole set was already built and promoted for \`${{ steps.vars.outputs.build_key }}\`."
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Stage the bundle in the host-local cache, once, for all the
|
||||||
|
# build jobs. Download to a temp name and mv for atomicity;
|
||||||
|
# prune stale bundles while at it.
|
||||||
|
mkdir -p "$BUNDLE_CACHE"
|
||||||
|
find "$BUNDLE_CACHE" -type f -mtime +1 -delete || true
|
||||||
|
ZIP="$BUNDLE_CACHE/penpot-${{ steps.vars.outputs.build_key }}.zip"
|
||||||
|
if [ ! -f "$ZIP" ]; then
|
||||||
|
aws s3 cp "s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip" "$ZIP.$$.tmp"
|
||||||
|
mv "$ZIP.$$.tmp" "$ZIP"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 2. One build per image, in parallel, only when needed ──────────────
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.image }}
|
||||||
|
runs-on: penpot-extended-runner
|
||||||
|
timeout-minutes: 60
|
||||||
|
needs: prepare
|
||||||
|
if: needs.prepare.outputs.exists == 'false'
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
# 4 runner slots are available for build jobs on this server; cap the
|
||||||
|
# matrix at 3 so short jobs (prepare and other workflows' checks)
|
||||||
|
# never queue behind long builds.
|
||||||
|
max-parallel: 3
|
||||||
|
matrix:
|
||||||
|
image: [backend, frontend, exporter, storybook, mcp]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set common environment variables
|
- name: Set common environment variables
|
||||||
run: |
|
run: |
|
||||||
# Each job execution will use its own docker configuration.
|
# Each job execution will use its own docker configuration.
|
||||||
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
|
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}-${{ matrix.image }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ inputs.gh_ref }}
|
ref: ${{ inputs.gh_ref }}
|
||||||
|
|
||||||
- name: Extract some useful variables
|
|
||||||
id: vars
|
|
||||||
run: |
|
|
||||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Download Penpot Bundles
|
|
||||||
id: bundles
|
|
||||||
env:
|
|
||||||
FILE_NAME: penpot-${{ steps.vars.outputs.gh_ref }}.zip
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
||||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
|
||||||
run: |
|
|
||||||
tmp=$(aws s3api head-object \
|
|
||||||
--bucket ${{ secrets.S3_BUCKET }} \
|
|
||||||
--key "$FILE_NAME" \
|
|
||||||
--query 'Metadata."bundle-version"' \
|
|
||||||
--output text)
|
|
||||||
echo "bundle_version=$tmp" >> $GITHUB_OUTPUT
|
|
||||||
pushd docker/images
|
|
||||||
aws s3 cp s3://${{ secrets.S3_BUCKET }}/$FILE_NAME .
|
|
||||||
unzip $FILE_NAME > /dev/null
|
|
||||||
mv penpot/backend bundle-backend
|
|
||||||
mv penpot/frontend bundle-frontend
|
|
||||||
mv penpot/exporter bundle-exporter
|
|
||||||
mv penpot/storybook bundle-storybook
|
|
||||||
mv penpot/mcp bundle-mcp
|
|
||||||
popd
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v4
|
|
||||||
|
|
||||||
- name: Login to Docker Registry
|
- name: Login to Docker Registry
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
@ -85,103 +147,140 @@ jobs:
|
|||||||
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
# Images now build FROM Docker Hardened Images (dhi.io). DHI
|
||||||
|
# is free (Apache 2.0, no subscription), but pulling from it
|
||||||
|
# still requires an authenticated login -- a separate `docker
|
||||||
|
# login` against a different registry host, even though it
|
||||||
|
# reuses the same PUB_DOCKER_* credentials as the DockerHub
|
||||||
|
# login above.
|
||||||
|
- name: Login to Docker Hardened Images registry (base image pull)
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: dhi.io
|
||||||
|
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
# Bundle staged once by `prepare` on this host; the S3 fallback only
|
||||||
|
# triggers if the cache is unavailable (runners on another machine,
|
||||||
|
# cache pruned mid-run, ...).
|
||||||
|
- name: Prepare Penpot bundle
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||||
|
run: |
|
||||||
|
ZIP="$BUNDLE_CACHE/penpot-${{ needs.prepare.outputs.build_key }}.zip"
|
||||||
|
if [ ! -f "$ZIP" ]; then
|
||||||
|
echo "Bundle not found in host cache; falling back to S3."
|
||||||
|
mkdir -p "$BUNDLE_CACHE"
|
||||||
|
aws s3 cp "s3://${{ secrets.S3_BUCKET }}/penpot-${{ needs.prepare.outputs.gh_ref }}.zip" "$ZIP.$$.tmp"
|
||||||
|
mv "$ZIP.$$.tmp" "$ZIP"
|
||||||
|
fi
|
||||||
|
# Extract only the bundle this job needs.
|
||||||
|
pushd docker/images
|
||||||
|
unzip -q "$ZIP" "penpot/${{ matrix.image }}/*"
|
||||||
|
mv "penpot/${{ matrix.image }}" "bundle-${{ matrix.image }}"
|
||||||
|
popd
|
||||||
|
|
||||||
|
- name: Set up QEMU (stable)
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels)
|
- name: Extract metadata (tags, labels)
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images:
|
images: ${{ matrix.image }}
|
||||||
frontend
|
|
||||||
backend
|
|
||||||
exporter
|
|
||||||
storybook
|
|
||||||
mcp
|
|
||||||
labels: |
|
labels: |
|
||||||
bundle_version=${{ steps.bundles.outputs.bundle_version }}
|
bundle_version=${{ needs.prepare.outputs.bundle_version }}
|
||||||
|
|
||||||
- name: Build and push Backend Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v7
|
||||||
env:
|
|
||||||
DOCKER_IMAGE: 'backend'
|
|
||||||
BUNDLE_PATH: './bundle-backend'
|
|
||||||
with:
|
with:
|
||||||
context: ./docker/images/
|
context: ./docker/images/
|
||||||
file: ./docker/images/Dockerfile.backend
|
file: ./docker/images/Dockerfile.${{ matrix.image }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
provenance: mode=max
|
||||||
|
sbom: true
|
||||||
|
# Immutable tag only; branch tags are moved atomically for the
|
||||||
|
# whole image set by the `promote` job.
|
||||||
|
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:build-${{ needs.prepare.outputs.build_key }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:buildcache
|
||||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:buildcache,mode=max
|
||||||
|
|
||||||
- name: Build and push Frontend Docker image
|
# ── 3. Move the branch tags of ALL images together ─────────────────────
|
||||||
uses: docker/build-push-action@v7
|
# Runs only when every build succeeded (default `needs` semantics); if
|
||||||
env:
|
# the set was already complete, `build` is skipped and so is this job —
|
||||||
DOCKER_IMAGE: 'frontend'
|
# the S3 marker guarantees the branch tags were already moved.
|
||||||
BUNDLE_PATH: './bundle-frontend'
|
promote:
|
||||||
|
name: Promote image set
|
||||||
|
runs-on: penpot-extended-runner
|
||||||
|
timeout-minutes: 10
|
||||||
|
needs: [prepare, build]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Set common environment variables
|
||||||
|
run: |
|
||||||
|
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Login to Docker Registry
|
||||||
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
context: ./docker/images/
|
registry: ${{ secrets.DOCKER_REGISTRY }}
|
||||||
file: ./docker/images/Dockerfile.frontend
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
platforms: linux/amd64,linux/arm64
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
push: true
|
|
||||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
|
||||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
|
||||||
|
|
||||||
- name: Build and push Exporter Docker image
|
- name: Point branch tags to the new build key
|
||||||
uses: docker/build-push-action@v7
|
run: |
|
||||||
|
set -e
|
||||||
|
for image in $ALL_IMAGES; do
|
||||||
|
docker buildx imagetools create \
|
||||||
|
-t "${{ secrets.DOCKER_REGISTRY }}/$image:${{ needs.prepare.outputs.gh_ref }}" \
|
||||||
|
"${{ secrets.DOCKER_REGISTRY }}/$image:build-${{ needs.prepare.outputs.build_key }}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# The marker is written LAST: its presence certifies that all five
|
||||||
|
# images exist and all branch tags point to this build key.
|
||||||
|
- name: Write set-completed marker
|
||||||
env:
|
env:
|
||||||
DOCKER_IMAGE: 'exporter'
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
BUNDLE_PATH: './bundle-exporter'
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
with:
|
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||||
context: ./docker/images/
|
run: |
|
||||||
file: ./docker/images/Dockerfile.exporter
|
echo "${{ github.run_id }}" | aws s3 cp - \
|
||||||
platforms: linux/amd64,linux/arm64
|
"s3://${{ secrets.S3_BUCKET }}/markers/images-${{ needs.prepare.outputs.build_key }}"
|
||||||
push: true
|
{
|
||||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
echo "### ✅ Image set promoted"
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
echo ""
|
||||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
echo "All \`:${{ needs.prepare.outputs.gh_ref }}\` tags now point to \`build-${{ needs.prepare.outputs.build_key }}\`."
|
||||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
- name: Build and push Storybook Docker image
|
# ── 4. Single failure notification for the whole workflow ─────────────
|
||||||
uses: docker/build-push-action@v7
|
notify:
|
||||||
env:
|
name: Notify failure
|
||||||
DOCKER_IMAGE: 'storybook'
|
runs-on: penpot-extended-runner
|
||||||
BUNDLE_PATH: './bundle-storybook'
|
timeout-minutes: 5
|
||||||
with:
|
needs: [prepare, build, promote]
|
||||||
context: ./docker/images/
|
|
||||||
file: ./docker/images/Dockerfile.storybook
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
|
||||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
|
||||||
|
|
||||||
- name: Build and push MCP Docker image
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
env:
|
|
||||||
DOCKER_IMAGE: 'mcp'
|
|
||||||
BUNDLE_PATH: './bundle-mcp'
|
|
||||||
with:
|
|
||||||
context: ./docker/images/
|
|
||||||
file: ./docker/images/Dockerfile.mcp
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
|
||||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
|
||||||
|
|
||||||
- name: Notify Mattermost
|
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: mattermost/action-mattermost-notify@master
|
|
||||||
|
steps:
|
||||||
|
- name: Notify Mattermost
|
||||||
|
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||||
with:
|
with:
|
||||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||||
TEXT: |
|
TEXT: |
|
||||||
❌ 🐳 *[PENPOT] Error building penpot docker images.*
|
❌ 🐳 *[PENPOT] Error building/promoting the penpot docker image set.*
|
||||||
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
|
📄 Triggered from ref: `${{ needs.prepare.outputs.gh_ref || inputs.gh_ref }}`
|
||||||
📦 Bundle: `${{ steps.bundles.outputs.bundle_version }}`
|
📦 Bundle: `${{ needs.prepare.outputs.bundle_version || 'n/a' }}`
|
||||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
@infra
|
@infra
|
||||||
|
|||||||
8
.github/workflows/build-staging.yml
vendored
8
.github/workflows/build-staging.yml
vendored
@ -11,8 +11,6 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
gh_ref: "staging"
|
gh_ref: "staging"
|
||||||
build_wasm: "yes"
|
|
||||||
build_storybook: "yes"
|
|
||||||
|
|
||||||
build-docker:
|
build-docker:
|
||||||
needs: build-bundle
|
needs: build-bundle
|
||||||
@ -20,3 +18,9 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
gh_ref: "staging"
|
gh_ref: "staging"
|
||||||
|
|
||||||
|
build-docker-admin-console:
|
||||||
|
uses: ./.github/workflows/build-docker-admin-console.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
gh_ref: "staging"
|
||||||
|
|||||||
19
.github/workflows/build-tag.yml
vendored
19
.github/workflows/build-tag.yml
vendored
@ -12,8 +12,6 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
gh_ref: ${{ github.ref_name }}
|
gh_ref: ${{ github.ref_name }}
|
||||||
build_wasm: "yes"
|
|
||||||
build_storybook: "yes"
|
|
||||||
|
|
||||||
build-docker:
|
build-docker:
|
||||||
needs: build-bundle
|
needs: build-bundle
|
||||||
@ -22,14 +20,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
gh_ref: ${{ github.ref_name }}
|
gh_ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
build-docker-admin-console:
|
||||||
|
uses: ./.github/workflows/build-docker-admin-console.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
gh_ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
notify:
|
notify:
|
||||||
name: Notifications
|
name: Notifications
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs: build-docker
|
needs:
|
||||||
|
- build-docker
|
||||||
|
- build-docker-admin-console
|
||||||
steps:
|
steps:
|
||||||
- name: Notify Mattermost
|
- name: Notify Mattermost
|
||||||
uses: mattermost/action-mattermost-notify@master
|
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||||
with:
|
with:
|
||||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||||
@ -40,7 +45,9 @@ jobs:
|
|||||||
|
|
||||||
publish-final-tag:
|
publish-final-tag:
|
||||||
if: ${{ !contains(github.ref_name, '-RC') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && contains(github.ref_name, '.') }}
|
if: ${{ !contains(github.ref_name, '-RC') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && contains(github.ref_name, '.') }}
|
||||||
needs: build-docker
|
needs:
|
||||||
|
- build-docker
|
||||||
|
- build-docker-admin-console
|
||||||
uses: ./.github/workflows/release.yml
|
uses: ./.github/workflows/release.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
|
|||||||
20
.github/workflows/build-tmp-tokens.yml
vendored
Normal file
20
.github/workflows/build-tmp-tokens.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
name: _TMP TOKENS
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '46 5-20 * * 1-5'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-bundle:
|
||||||
|
uses: ./.github/workflows/build-bundle.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
gh_ref: "hiru-tokens-in-libs"
|
||||||
|
|
||||||
|
build-docker:
|
||||||
|
needs: build-bundle
|
||||||
|
uses: ./.github/workflows/build-docker.yml
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
gh_ref: "hiru-tokens-in-libs"
|
||||||
2
.github/workflows/plugins-deploy-api-doc.yml
vendored
2
.github/workflows/plugins-deploy-api-doc.yml
vendored
@ -131,7 +131,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Notify Mattermost
|
- name: Notify Mattermost
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: mattermost/action-mattermost-notify@master
|
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||||
with:
|
with:
|
||||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||||
|
|||||||
4
.github/workflows/plugins-deploy-package.yml
vendored
4
.github/workflows/plugins-deploy-package.yml
vendored
@ -34,7 +34,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: penpot-runner-01
|
runs-on: penpot-standar-runner
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@ -114,7 +114,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Notify Mattermost
|
- name: Notify Mattermost
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: mattermost/action-mattermost-notify@master
|
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||||
with:
|
with:
|
||||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||||
|
|||||||
@ -129,7 +129,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Notify Mattermost
|
- name: Notify Mattermost
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: mattermost/action-mattermost-notify@master
|
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||||
with:
|
with:
|
||||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -103,7 +103,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Notify Mattermost
|
- name: Notify Mattermost
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: mattermost/action-mattermost-notify@master
|
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||||
with:
|
with:
|
||||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||||
|
|||||||
15
.github/workflows/tests-backend.yml
vendored
15
.github/workflows/tests-backend.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
|||||||
test-backend:
|
test-backend:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
if: ${{ !github.event.pull_request.draft }}
|
||||||
name: "Backend Tests"
|
name: "Backend Tests"
|
||||||
runs-on: penpot-runner-02
|
runs-on: penpot-extended-runner
|
||||||
container:
|
container:
|
||||||
image: penpotapp/devenv:latest
|
image: penpotapp/devenv:latest
|
||||||
volumes:
|
volumes:
|
||||||
@ -62,14 +62,15 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Fmt
|
||||||
|
working-directory: ./backend
|
||||||
|
run: |
|
||||||
|
cljfmt check --parallel=true src/ test/
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
working-directory: ./backend
|
working-directory: ./backend
|
||||||
run: |
|
run: |
|
||||||
corepack enable;
|
clj-kondo --parallel --lint ../common/src/ src/
|
||||||
corepack install;
|
|
||||||
pnpm install;
|
|
||||||
pnpm run check-fmt
|
|
||||||
pnpm run lint
|
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
working-directory: ./backend
|
working-directory: ./backend
|
||||||
@ -81,4 +82,4 @@ jobs:
|
|||||||
|
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /tmp/penpot;
|
mkdir -p /tmp/penpot;
|
||||||
clojure -M:dev:test --reporter kaocha.report/documentation
|
clojure -M:dev:test
|
||||||
|
|||||||
2
.github/workflows/tests-common.yml
vendored
2
.github/workflows/tests-common.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
|||||||
test-common:
|
test-common:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
if: ${{ !github.event.pull_request.draft }}
|
||||||
name: "Common Tests"
|
name: "Common Tests"
|
||||||
runs-on: penpot-runner-02
|
runs-on: penpot-extended-runner
|
||||||
container:
|
container:
|
||||||
image: penpotapp/devenv:latest
|
image: penpotapp/devenv:latest
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
69
.github/workflows/tests-composable-suite.yml
vendored
Normal file
69
.github/workflows/tests-composable-suite.yml
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
name: "CI: Composable Test Suite"
|
||||||
|
|
||||||
|
# Runs the composable component test suite (it exercises component semantics
|
||||||
|
# through the real Plugin API against the full frontend, so it needs the
|
||||||
|
# frontend bundle + the plugin runtime, but no backend): the driver serves the
|
||||||
|
# prebuilt frontend bundle and intercepts every backend RPC with Playwright
|
||||||
|
# fixtures. See plugins/apps/composable-test-suite/README.md ("Running in CI").
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'plugins/**'
|
||||||
|
- 'frontend/**'
|
||||||
|
- 'common/**'
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- ready_for_review
|
||||||
|
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- staging
|
||||||
|
paths:
|
||||||
|
- 'plugins/**'
|
||||||
|
- 'frontend/**'
|
||||||
|
- 'common/**'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
composable-test-suite:
|
||||||
|
if: ${{ !github.event.pull_request.draft }}
|
||||||
|
name: "Run composable test suite (mocked backend)"
|
||||||
|
runs-on: penpot-extended-runner
|
||||||
|
container:
|
||||||
|
image: penpotapp/devenv:latest
|
||||||
|
volumes:
|
||||||
|
- /var/cache/github-runner/m2:/root/.m2
|
||||||
|
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
# The driver serves the prebuilt bundle from frontend/resources/public.
|
||||||
|
- name: Build frontend bundle
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: ./scripts/build
|
||||||
|
|
||||||
|
- name: Install deps
|
||||||
|
working-directory: ./plugins
|
||||||
|
run: |
|
||||||
|
corepack enable;
|
||||||
|
corepack install;
|
||||||
|
pnpm install;
|
||||||
|
|
||||||
|
- name: Install Playwright Chromium
|
||||||
|
working-directory: ./plugins
|
||||||
|
run: pnpm --filter composable-test-suite exec playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Run composable test suite (mocked)
|
||||||
|
working-directory: ./plugins
|
||||||
|
run: pnpm --filter composable-test-suite run test:ci
|
||||||
58
.github/workflows/tests-exporter.yml
vendored
Normal file
58
.github/workflows/tests-exporter.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
name: "CI: Exporter"
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'exporter/**'
|
||||||
|
- 'common/**'
|
||||||
|
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- ready_for_review
|
||||||
|
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- staging
|
||||||
|
|
||||||
|
paths:
|
||||||
|
- 'exporter/**'
|
||||||
|
- 'common/**'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-exporter:
|
||||||
|
if: ${{ !github.event.pull_request.draft }}
|
||||||
|
name: "Exporter Tests"
|
||||||
|
runs-on: penpot-runner-02
|
||||||
|
container:
|
||||||
|
image: penpotapp/devenv:latest
|
||||||
|
volumes:
|
||||||
|
- /var/cache/github-runner/m2:/root/.m2
|
||||||
|
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
working-directory: ./exporter
|
||||||
|
run: |
|
||||||
|
corepack enable;
|
||||||
|
corepack install;
|
||||||
|
pnpm install;
|
||||||
|
pnpm run check-fmt:clj
|
||||||
|
pnpm run lint:clj
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
working-directory: ./exporter
|
||||||
|
run: |
|
||||||
|
./scripts/test
|
||||||
2
.github/workflows/tests-frontend.yml
vendored
2
.github/workflows/tests-frontend.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
|||||||
test-frontend:
|
test-frontend:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
if: ${{ !github.event.pull_request.draft }}
|
||||||
name: "Frontend Tests"
|
name: "Frontend Tests"
|
||||||
runs-on: penpot-runner-02
|
runs-on: penpot-extended-runner
|
||||||
container:
|
container:
|
||||||
image: penpotapp/devenv:latest
|
image: penpotapp/devenv:latest
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
192
.github/workflows/tests-integration.yml
vendored
192
.github/workflows/tests-integration.yml
vendored
@ -5,11 +5,37 @@ defaults:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
gh_ref:
|
||||||
|
description: 'Name of the branch or ref'
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
default: 'develop'
|
||||||
|
|
||||||
|
shards:
|
||||||
|
description: 'Shard layout (JSON array)'
|
||||||
|
type: choice
|
||||||
|
required: true
|
||||||
|
default: '[1, 2, 3, 4]'
|
||||||
|
options:
|
||||||
|
- '[1, 2, 3, 4]'
|
||||||
|
- '[1, 2, 3, 4, 5, 6]'
|
||||||
|
- '[1, 2]'
|
||||||
|
- '[1]'
|
||||||
|
|
||||||
|
workers:
|
||||||
|
description: 'Playwright workers per shard'
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
default: '2'
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'frontend/**'
|
- 'frontend/**'
|
||||||
- 'common/**'
|
- 'common/**'
|
||||||
- 'render-wasm/**'
|
- 'render-wasm/**'
|
||||||
|
- '.github/workflows/tests-integration.yml'
|
||||||
|
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
@ -25,25 +51,41 @@ on:
|
|||||||
- 'frontend/**'
|
- 'frontend/**'
|
||||||
- 'common/**'
|
- 'common/**'
|
||||||
- 'render-wasm/**'
|
- 'render-wasm/**'
|
||||||
|
- '.github/workflows/tests-integration.yml'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.gh_ref || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-integration:
|
build-integration:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
if: ${{ !github.event.pull_request.draft }}
|
||||||
name: "Build Integration Bundle"
|
name: "Build Integration Bundle"
|
||||||
runs-on: penpot-runner-02
|
runs-on: penpot-extended-runner
|
||||||
|
timeout-minutes: 30
|
||||||
container:
|
container:
|
||||||
image: penpotapp/devenv:latest
|
image: penpotapp/devenv:latest
|
||||||
volumes:
|
volumes:
|
||||||
- /var/cache/github-runner/m2:/root/.m2
|
- /var/cache/github-runner/m2:/root/.m2
|
||||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
bundle_key: ${{ steps.vars.outputs.bundle_key }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
# An empty `ref` makes checkout fall back to its default (the PR merge
|
||||||
|
# ref on pull_request, the pushed ref on push).
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.gh_ref }}
|
||||||
|
|
||||||
|
# The cache key must come from the SHA actually checked out: on a manual
|
||||||
|
# run `github.sha` points at the dispatching ref, not at `gh_ref`.
|
||||||
|
- name: Extract cache key
|
||||||
|
id: vars
|
||||||
|
run: |
|
||||||
|
echo "bundle_key=integration-bundle-$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build Bundle
|
- name: Build Bundle
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
@ -53,41 +95,151 @@ jobs:
|
|||||||
- name: Store Bundle Cache
|
- name: Store Bundle Cache
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
key: "integration-bundle-${{ github.sha }}"
|
key: ${{ steps.vars.outputs.bundle_key }}
|
||||||
path: frontend/resources/public
|
path: frontend/resources/public
|
||||||
|
|
||||||
test-integration:
|
test-integration:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
if: ${{ !github.event.pull_request.draft }}
|
||||||
name: "Integration Tests"
|
name: "Integration Tests (${{ matrix.shard }})"
|
||||||
runs-on: penpot-runner-02
|
runs-on: penpot-extended-runner
|
||||||
|
timeout-minutes: ${{ github.base_ref == 'staging' && 60 || 25 }}
|
||||||
|
|
||||||
|
needs: build-integration
|
||||||
|
|
||||||
|
# TEMPORARY (release stabilization): PRs targeting `staging` run on a
|
||||||
|
# single serial shard, so new flakes cannot block the release work.
|
||||||
|
# Remove the `github.base_ref` branch below to restore full parallelism.
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
shard: ${{ fromJSON(inputs.shards || (github.base_ref == 'staging' && '[1]' || '[1, 2, 3, 4]')) }}
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: penpotapp/devenv:latest
|
||||||
|
volumes:
|
||||||
|
- /var/cache/github-runner/m2:/root/.m2
|
||||||
|
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||||
|
- /var/cache/github-runner/ms-playwright:/ms-playwright
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.gh_ref }}
|
||||||
|
|
||||||
|
- name: Restore Cache
|
||||||
|
uses: actions/cache/restore@v5
|
||||||
|
with:
|
||||||
|
key: ${{ needs.build-integration.outputs.bundle_key }}
|
||||||
|
path: frontend/resources/public
|
||||||
|
|
||||||
|
- name: Install deps
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: |
|
||||||
|
corepack enable;
|
||||||
|
corepack install;
|
||||||
|
pnpm install --frozen-lockfile;
|
||||||
|
|
||||||
|
# No-op once the shared volume is warm; keeps the first run working.
|
||||||
|
- name: Install Playwright Chromium
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: pnpm exec playwright install chromium
|
||||||
|
|
||||||
|
# `strategy.job-total` is the matrix size, so the shard denominator
|
||||||
|
# follows the `shards` input without being hardcoded.
|
||||||
|
- name: Run Tests
|
||||||
|
working-directory: ./frontend
|
||||||
|
env:
|
||||||
|
WORKERS: ${{ inputs.workers }}
|
||||||
|
BASE_REF: ${{ github.base_ref }}
|
||||||
|
run: |
|
||||||
|
# TEMPORARY (release stabilization): see the note on the matrix above.
|
||||||
|
if [ -z "$WORKERS" ]; then
|
||||||
|
if [ "$BASE_REF" = "staging" ]; then WORKERS=1; else WORKERS=2; fi
|
||||||
|
fi
|
||||||
|
echo "Running shard ${{ matrix.shard }}/${{ strategy.job-total }} with $WORKERS workers"
|
||||||
|
pnpm exec playwright test --project default \
|
||||||
|
--workers="$WORKERS" \
|
||||||
|
--shard=${{ matrix.shard }}/${{ strategy.job-total }} \
|
||||||
|
--reporter=blob
|
||||||
|
|
||||||
|
- name: Upload blob report
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: integration-blob-report-${{ matrix.shard }}
|
||||||
|
path: frontend/blob-report/
|
||||||
|
overwrite: true
|
||||||
|
retention-days: 3
|
||||||
|
|
||||||
|
- name: Upload test result
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: integration-tests-result-${{ matrix.shard }}
|
||||||
|
path: frontend/test-results/
|
||||||
|
overwrite: true
|
||||||
|
if-no-files-found: ignore
|
||||||
|
retention-days: 3
|
||||||
|
|
||||||
|
merge-reports:
|
||||||
|
if: ${{ always() && !github.event.pull_request.draft && needs.test-integration.result != 'skipped' }}
|
||||||
|
name: "Merge Integration Reports"
|
||||||
|
runs-on: penpot-extended-runner
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
needs: test-integration
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: penpotapp/devenv:latest
|
image: penpotapp/devenv:latest
|
||||||
volumes:
|
volumes:
|
||||||
- /var/cache/github-runner/m2:/root/.m2
|
- /var/cache/github-runner/m2:/root/.m2
|
||||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||||
|
|
||||||
needs: build-integration
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Restore Cache
|
|
||||||
uses: actions/cache/restore@v5
|
|
||||||
with:
|
with:
|
||||||
key: "integration-bundle-${{ github.sha }}"
|
ref: ${{ inputs.gh_ref }}
|
||||||
path: frontend/resources/public
|
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Install deps
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
run: |
|
run: |
|
||||||
./scripts/test-e2e
|
corepack enable;
|
||||||
|
corepack install;
|
||||||
|
pnpm install --frozen-lockfile;
|
||||||
|
|
||||||
- name: Upload test result
|
- name: Download blob reports
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/download-artifact@v7
|
||||||
if: always()
|
|
||||||
with:
|
with:
|
||||||
name: integration-tests-result
|
path: frontend/all-blob-reports
|
||||||
path: frontend/test-results/
|
pattern: integration-blob-report-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Merge into HTML report
|
||||||
|
working-directory: ./frontend
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_JSON_OUTPUT_NAME: report.json
|
||||||
|
run: |
|
||||||
|
pnpm exec playwright merge-reports \
|
||||||
|
--reporter=html,json,list ./all-blob-reports
|
||||||
|
|
||||||
|
- name: Test summary
|
||||||
|
if: always()
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: |
|
||||||
|
if [ ! -f report.json ]; then
|
||||||
|
echo "No report produced (all shards failed early)." >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
jq -r -f ../.github/scripts/playwright-summary.jq report.json >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
- name: Upload HTML report
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: integration-html-report
|
||||||
|
path: frontend/playwright-report/
|
||||||
overwrite: true
|
overwrite: true
|
||||||
retention-days: 3
|
retention-days: 7
|
||||||
|
|||||||
2
.github/workflows/tests-library.yml
vendored
2
.github/workflows/tests-library.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
|||||||
test-library:
|
test-library:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
if: ${{ !github.event.pull_request.draft }}
|
||||||
name: "Library Tests"
|
name: "Library Tests"
|
||||||
runs-on: penpot-runner-02
|
runs-on: penpot-extended-runner
|
||||||
container:
|
container:
|
||||||
image: penpotapp/devenv:latest
|
image: penpotapp/devenv:latest
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
4
.github/workflows/tests-mcp.yml
vendored
4
.github/workflows/tests-mcp.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: "MCP CI"
|
name: "CI: MCP"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
@ -28,7 +28,7 @@ jobs:
|
|||||||
test-mcp:
|
test-mcp:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
if: ${{ !github.event.pull_request.draft }}
|
||||||
name: "Test MCP"
|
name: "Test MCP"
|
||||||
runs-on: penpot-runner-02
|
runs-on: penpot-extended-runner
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
4
.github/workflows/tests-plugin-api-suite.yml
vendored
4
.github/workflows/tests-plugin-api-suite.yml
vendored
@ -53,7 +53,7 @@ jobs:
|
|||||||
api-test-suite-mocked:
|
api-test-suite-mocked:
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
|
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
|
||||||
name: "Run Plugin API Test Suite (mocked)"
|
name: "Run Plugin API Test Suite (mocked)"
|
||||||
runs-on: penpot-runner-02
|
runs-on: penpot-extended-runner
|
||||||
container:
|
container:
|
||||||
image: penpotapp/devenv:latest
|
image: penpotapp/devenv:latest
|
||||||
volumes:
|
volumes:
|
||||||
@ -95,7 +95,7 @@ jobs:
|
|||||||
# api-test-suite-live:
|
# api-test-suite-live:
|
||||||
# if: ${{ github.event_name == 'workflow_dispatch' }}
|
# if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
# name: Run Plugin API Test Suite (live)
|
# name: Run Plugin API Test Suite (live)
|
||||||
# runs-on: penpot-runner-02
|
# runs-on: penpot-extended-runner
|
||||||
# container:
|
# container:
|
||||||
# image: penpotapp/devenv:latest
|
# image: penpotapp/devenv:latest
|
||||||
#
|
#
|
||||||
|
|||||||
2
.github/workflows/tests-plugins.yml
vendored
2
.github/workflows/tests-plugins.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
|||||||
test-plugins:
|
test-plugins:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
if: ${{ !github.event.pull_request.draft }}
|
||||||
name: Plugins Runtime Linter & Tests
|
name: Plugins Runtime Linter & Tests
|
||||||
runs-on: penpot-runner-02
|
runs-on: penpot-extended-runner
|
||||||
container:
|
container:
|
||||||
image: penpotapp/devenv:latest
|
image: penpotapp/devenv:latest
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
2
.github/workflows/tests-wasm.yml
vendored
2
.github/workflows/tests-wasm.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
|||||||
test-render-wasm:
|
test-render-wasm:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
if: ${{ !github.event.pull_request.draft }}
|
||||||
name: "Render WASM Tests"
|
name: "Render WASM Tests"
|
||||||
runs-on: penpot-runner-02
|
runs-on: penpot-extended-runner
|
||||||
container:
|
container:
|
||||||
image: penpotapp/devenv:latest
|
image: penpotapp/devenv:latest
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -9,6 +9,7 @@
|
|||||||
.clj-kondo
|
.clj-kondo
|
||||||
.cpcache
|
.cpcache
|
||||||
.lsp
|
.lsp
|
||||||
|
.env
|
||||||
.nrepl-port
|
.nrepl-port
|
||||||
.nyc_output
|
.nyc_output
|
||||||
.rebel_readline_history
|
.rebel_readline_history
|
||||||
@ -57,6 +58,8 @@ opencode.json
|
|||||||
/docker/images/bundle*
|
/docker/images/bundle*
|
||||||
/exporter/target
|
/exporter/target
|
||||||
/exporter/.shadow-cljs
|
/exporter/.shadow-cljs
|
||||||
|
/exporter/resources/wasm/
|
||||||
|
/exporter/src/app/wasm/shared.js
|
||||||
/frontend/.storybook/preview-body.html
|
/frontend/.storybook/preview-body.html
|
||||||
/frontend/.storybook/preview-head.html
|
/frontend/.storybook/preview-head.html
|
||||||
/frontend/playwright-report/
|
/frontend/playwright-report/
|
||||||
@ -73,8 +76,6 @@ opencode.json
|
|||||||
/frontend/target/
|
/frontend/target/
|
||||||
/frontend/test-results/
|
/frontend/test-results/
|
||||||
/frontend/.shadow-cljs
|
/frontend/.shadow-cljs
|
||||||
/other/
|
|
||||||
/scripts/
|
|
||||||
/nexus/
|
/nexus/
|
||||||
/tmp/
|
/tmp/
|
||||||
/vendor/**/target
|
/vendor/**/target
|
||||||
@ -89,6 +90,7 @@ opencode.json
|
|||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
/render-wasm/target/
|
/render-wasm/target/
|
||||||
|
/media-processor/dist/
|
||||||
/**/node_modules
|
/**/node_modules
|
||||||
/**/.yarn/*
|
/**/.yarn/*
|
||||||
/.pnpm-store
|
/.pnpm-store
|
||||||
@ -102,5 +104,6 @@ opencode.json
|
|||||||
/.opencode/plans
|
/.opencode/plans
|
||||||
/.opencode/reports
|
/.opencode/reports
|
||||||
/.opencode/prompts
|
/.opencode/prompts
|
||||||
|
/.ci-logs
|
||||||
/.codex/
|
/.codex/
|
||||||
/tools/__pycache__
|
/tools/__pycache__
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
name: commiter
|
|
||||||
description: Git commit assistant
|
|
||||||
mode: subagent
|
|
||||||
permission:
|
|
||||||
read: allow
|
|
||||||
glob: allow
|
|
||||||
grep: allow
|
|
||||||
edit: deny
|
|
||||||
webfetch: deny
|
|
||||||
websearch: deny
|
|
||||||
task: deny
|
|
||||||
skill: deny
|
|
||||||
lsp: deny
|
|
||||||
todowrite: deny
|
|
||||||
question: deny
|
|
||||||
external_directory: deny
|
|
||||||
bash: allow
|
|
||||||
---
|
|
||||||
|
|
||||||
## Role
|
|
||||||
|
|
||||||
You are the Penpot commit assistant. You produce git commits that follow the
|
|
||||||
repository's commit conventions. You do not implement features, review code, or
|
|
||||||
push branches — you commit.
|
|
||||||
|
|
||||||
## Required Reading
|
|
||||||
|
|
||||||
Before drafting any commit, **read `.serena/memories/workflow/creating-commits.md`
|
|
||||||
end-to-end**. It is the authoritative source for the commit message format, the
|
|
||||||
emoji menu, subject/body limits, and the `AI-assisted-by` trailer. Follow it
|
|
||||||
exactly — do not improvise the format and do not restate its contents here.
|
|
||||||
|
|
||||||
## Pre-commit Workflow
|
|
||||||
|
|
||||||
1. **Stage the files** specified by the calling agent. Do not ask for
|
|
||||||
confirmation — the calling agent knows exactly which files to commit.
|
|
||||||
2. Run `git diff --staged` to review the content. If you see secrets (API
|
|
||||||
keys, tokens, passwords, private keys, `.env` values), debug prints, or
|
|
||||||
anything that does not match the stated intent, STOP and tell the user
|
|
||||||
before committing.
|
|
||||||
3. Following the format in the doc, draft the message and run
|
|
||||||
`git commit -m "<subject>" -m "<body>"` (or `git commit -F -` if the body has
|
|
||||||
unusual characters). The `AI-assisted-by` trailer value is provided by the
|
|
||||||
calling agent — use it verbatim.
|
|
||||||
|
|
||||||
## Constraints
|
|
||||||
|
|
||||||
- Do not push. Pushing is a separate workflow handled by the user.
|
|
||||||
- Do not run `git reset`, `git checkout`, `git restore`, `git clean`, or `rm` — these are destructive operations.
|
|
||||||
- Do not pass `--author`. Author identity comes from the local git config.
|
|
||||||
- Do not amend a commit you did not create in this session, unless the user explicitly asks.
|
|
||||||
- Do not bypass pre-commit hooks (`--no-verify`) unless the user explicitly asks.
|
|
||||||
- Do not add untracked files that were not created in this session.
|
|
||||||
- Do not ask questions. The calling agent provides all necessary information. If something is unclear, proceed with what you know and note any assumptions in your response.
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
description: Execute a ready plan end-to-end — create a GitHub issue, branch issue-NNNN, implement the plan, then commit via the commiter subagent
|
description: Execute a ready plan end-to-end — create a GitHub issue, branch issue-NNNN, implement the plan, then commit via the create-commit skill
|
||||||
agent: build
|
agent: build
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -32,12 +32,11 @@ Implement the prepared plan from the session context. Work methodically, keeping
|
|||||||
changes focused on what the issue requires. Do not commit — the commit happens in
|
changes focused on what the issue requires. Do not commit — the commit happens in
|
||||||
step 4.
|
step 4.
|
||||||
|
|
||||||
## 4. Commit with the commiter subagent
|
## 4. Commit with the create-commit skill
|
||||||
|
|
||||||
After the implementation is complete, delegate the commit to the **`commiter`**
|
After the implementation is complete, load the **`create-commit`** skill and
|
||||||
subagent. Give it a brief summary of what was implemented and why, the issue
|
follow its workflow to commit the changes. Provide a brief summary of what was
|
||||||
reference (`issue-NNNN`), and the model name you are running as so it sets the
|
implemented and why, the issue reference (`issue-NNNN`), and the model name you
|
||||||
`AI-assisted-by` trailer correctly. The subagent owns the commit format and
|
are running as so the `AI-assisted-by` trailer is set correctly.
|
||||||
conventions.
|
|
||||||
|
|
||||||
Do not push. Pushing is handled separately by the user.
|
Do not push. Pushing is handled separately by the user.
|
||||||
|
|||||||
40
.opencode/commands/resolve-git-conflicts.md
Normal file
40
.opencode/commands/resolve-git-conflicts.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
description: Resolve local git conflicts and stage the resolved files with git add — never continues the rebase
|
||||||
|
agent: build
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fix Git Conflicts
|
||||||
|
|
||||||
|
Resolve conflicts in the local repository. The user handles finishing the
|
||||||
|
rebase themselves — you must **never** run `git rebase --continue`,
|
||||||
|
`git rebase --skip`, `git merge --continue`, or anything similar.
|
||||||
|
|
||||||
|
## Phase 1 — Understand the problem (read-only)
|
||||||
|
|
||||||
|
1. Run `git status` to detect the conflict state (rebase, merge, cherry-pick, etc.) and list conflicted files.
|
||||||
|
2. For each conflicted (unmerged) file, understand the situation **without modifying anything**:
|
||||||
|
- Read the file and identify the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`).
|
||||||
|
- Inspect both sides — `git show <ours>:<file>` and `git show <theirs>:<file>` — plus `git log`/`git show` on the commits involved to understand intent.
|
||||||
|
- Identify what each side changed and why, and how they should be combined.
|
||||||
|
|
||||||
|
## Phase 2 — Present the resolution plan
|
||||||
|
|
||||||
|
3. **Present a clear plan to the user before touching any file.** For each conflicted file, state:
|
||||||
|
- What each side changed and why.
|
||||||
|
- Your proposed resolution and the reasoning behind it.
|
||||||
|
- How the two sides are combined (both additive → merge; both modify the same code → keep the semantically correct version, merging intent from both sides when clear from code and context).
|
||||||
|
4. **Ask the user only when genuinely unclear.** Do not ask about anything you can determine yourself from the code, commit messages, or context. Only decisions that are not determinable and change the outcome (e.g. conflicting product decisions, which side to discard) warrant a question. **Collect all such questions together in an "Open Questions" section at the end of the plan**, so the user has full context to answer them properly.
|
||||||
|
5. **Wait for the user to accept the plan** (and answer any open questions) before editing, staging, or otherwise modifying anything.
|
||||||
|
|
||||||
|
## Phase 3 — Execute
|
||||||
|
|
||||||
|
6. Resolve each conflicted file by editing the file to the agreed merged content and removing all conflict markers.
|
||||||
|
|
||||||
|
## Phase 4 — Stage and verify
|
||||||
|
|
||||||
|
7. **Stage every resolved file** with `git add <file>`. Do not stage unrelated untracked files unless clearly part of the resolution.
|
||||||
|
8. Verify no conflict markers remain (search for `<<<<<<<` / `>>>>>>>` in resolved files) and that `git status` shows no unmerged paths.
|
||||||
|
|
||||||
|
## Phase 5 — Report
|
||||||
|
|
||||||
|
9. Briefly report the conflict state, how each conflicted file was resolved (and any answers received to open questions), and stop — do **not** run `git rebase --continue` or any other continuation command.
|
||||||
@ -1,21 +1,25 @@
|
|||||||
---
|
Act as a senior software engineer and perform a thorough review.
|
||||||
description: Review a commit (defaults to the last commit) with the code-review-and-quality skill across all five axes
|
|
||||||
agent: plan
|
|
||||||
subtask: true
|
|
||||||
---
|
|
||||||
|
|
||||||
You are performing a code review of a git commit. You MUST conduct it using the **`code-review-and-quality`** skill (the five-axis review: correctness, readability, architecture, security, performance).
|
## Instructions
|
||||||
|
|
||||||
The user may specify a commit or revision range as an argument ($ARGUMENTS). If no argument is given, default to reviewing the **last commit** (`HEAD`, i.e. the changes introduced by `HEAD` vs its parent).
|
1. **Determine what is being reviewed** from the provided context:
|
||||||
|
- **If it is a plan** (implementation plan, design document, task breakdown) → load the **`plan-review`** skill.
|
||||||
|
- **If it is code** (diff, PR, code change) → load the **`code-review`** skill.
|
||||||
|
|
||||||
Workflow:
|
2. Read `AGENTS.md` and follow its instructions for finding and reading all related testing documentation from memories before reviewing.
|
||||||
|
|
||||||
1. Determine the target to review:
|
3. **Skip generated files, lockfile-only changes, and unrelated modifications** unless they introduce security risks.
|
||||||
- If the user provided a revision/range in $ARGUMENTS, use it.
|
|
||||||
- Otherwise, default to the last commit: review `HEAD` (the diff of `HEAD` against `HEAD~1`).
|
|
||||||
2. Inspect the change with `git show <target>` / `git diff <target>~1 <target>` and `git log -1 --stat <target>` to understand the intent and the files touched.
|
|
||||||
3. Invoke the **`code-review-and-quality`** skill and review the commit across all five axes. Categorize every finding as Critical / Required / Optional / Nit / FYI, and lead with correctness and security.
|
|
||||||
4. For each finding, state the axis it belongs to, the severity, and a concrete suggested fix (propose the structural remedy, not just the problem).
|
|
||||||
5. Conclude with a clear verdict: **Approve** (ready to merge) or **Request changes** (issues that must be addressed), and summarize the highest-leverage items.
|
|
||||||
|
|
||||||
Do not modify any code and do not create a commit — this command only reviews.
|
4. Follow the loaded skill's process and produce its output format.
|
||||||
|
|
||||||
|
## Strong Rules
|
||||||
|
|
||||||
|
1. Do not invent problems. Every finding must be real and actionable.
|
||||||
|
2. Do not modify any code and do not create a commit — this command only reviews.
|
||||||
|
3. Be specific and constructive. "This could be better" is not helpful — explain why and how.
|
||||||
|
4. Prioritize by impact. One structural issue outweighs ten nits.
|
||||||
|
5. Missing tests are an issue, not a suggestion. Report as a severity-tagged finding — never as a recommendation.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
$ARGUMENTS
|
||||||
|
|||||||
299
.opencode/plugins/penpot.js
Normal file
299
.opencode/plugins/penpot.js
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
import { tool } from "@opencode-ai/plugin"
|
||||||
|
import path from "path"
|
||||||
|
import { spawn } from "child_process"
|
||||||
|
|
||||||
|
const penpotPsqlTool = tool({
|
||||||
|
description:
|
||||||
|
"Execute a SQL command against the Penpot database. Uses the defaults from scripts/psql.",
|
||||||
|
|
||||||
|
args: {
|
||||||
|
sql: tool.schema
|
||||||
|
.string()
|
||||||
|
.describe("SQL command to execute"),
|
||||||
|
|
||||||
|
test: tool.schema
|
||||||
|
.boolean()
|
||||||
|
.describe("Use the penpot_test database")
|
||||||
|
.optional(),
|
||||||
|
},
|
||||||
|
|
||||||
|
async execute(args, context) {
|
||||||
|
const host = process.env.PENPOT_DB_HOST || "postgres"
|
||||||
|
const user = process.env.PENPOT_DB_USER || "penpot"
|
||||||
|
const db = args.test
|
||||||
|
? "penpot_test"
|
||||||
|
: process.env.PENPOT_DB_NAME || "penpot"
|
||||||
|
const password = process.env.PENPOT_DB_PASSWORD || "penpot"
|
||||||
|
|
||||||
|
const psqlArgs = ["-h", host, "-U", user, "-d", db, "-c", args.sql]
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let stdout = ""
|
||||||
|
let stderr = ""
|
||||||
|
|
||||||
|
const proc = spawn("psql", psqlArgs, {
|
||||||
|
cwd: context.worktree,
|
||||||
|
env: { ...process.env, PGPASSWORD: password },
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.stdout.on("data", (data) => {
|
||||||
|
stdout += data.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.stderr.on("data", (data) => {
|
||||||
|
stderr += data.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.on("error", (error) => {
|
||||||
|
resolve(`Error: ${error.message}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.on("close", (exitCode) => {
|
||||||
|
const output =
|
||||||
|
exitCode === 0
|
||||||
|
? stdout.trim() || "Query executed successfully"
|
||||||
|
: `Error (exit ${exitCode}): ${
|
||||||
|
(stderr || stdout).trim() || "No error output"
|
||||||
|
}`
|
||||||
|
resolve(output)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const parenRepairTool = tool({
|
||||||
|
description:
|
||||||
|
"Fix mismatched parentheses/braces in Clojure files (.clj, .cljs, .cljc) then reformat with cljfmt.",
|
||||||
|
|
||||||
|
args: {
|
||||||
|
// A string is used instead of an array so OpenCode displays it
|
||||||
|
// in the generic tool invocation.
|
||||||
|
files: tool.schema
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"Comma-separated file paths to fix, for example: frontend/src/app/config.cljs, backend/src/core.clj",
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
code: tool.schema
|
||||||
|
.string()
|
||||||
|
.describe("Code string to fix via stdin")
|
||||||
|
.optional(),
|
||||||
|
},
|
||||||
|
|
||||||
|
async execute(args, context) {
|
||||||
|
const script = path.join(context.worktree, "scripts/paren-repair")
|
||||||
|
|
||||||
|
const files = args.files
|
||||||
|
? args.files
|
||||||
|
.split(",")
|
||||||
|
.map((file) => file.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: []
|
||||||
|
|
||||||
|
const paramInfo =
|
||||||
|
files.length > 0
|
||||||
|
? `files=[${files.join(", ")}]`
|
||||||
|
: args.code !== undefined
|
||||||
|
? `code=(${args.code.length} chars)`
|
||||||
|
: "none"
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const childArgs =
|
||||||
|
files.length > 0
|
||||||
|
? [script, ...files]
|
||||||
|
: [script]
|
||||||
|
|
||||||
|
const proc = spawn("bb", childArgs, {
|
||||||
|
cwd: context.worktree,
|
||||||
|
})
|
||||||
|
|
||||||
|
let stdout = ""
|
||||||
|
let stderr = ""
|
||||||
|
|
||||||
|
proc.stdout.on("data", (data) => {
|
||||||
|
stdout += data.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.stderr.on("data", (data) => {
|
||||||
|
stderr += data.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.on("error", (error) => {
|
||||||
|
resolve(`Error: ${error.message}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.on("close", (exitCode) => {
|
||||||
|
const output =
|
||||||
|
exitCode === 0
|
||||||
|
? stdout.trim() || "No changes needed"
|
||||||
|
: `Error (exit ${exitCode}): ${
|
||||||
|
(stderr || stdout).trim() || "No error output"
|
||||||
|
}`
|
||||||
|
|
||||||
|
resolve(output)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Close stdin in all cases so the process cannot wait indefinitely.
|
||||||
|
if (args.code !== undefined) {
|
||||||
|
proc.stdin.end(args.code)
|
||||||
|
} else {
|
||||||
|
proc.stdin.end()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default async function plugin() {
|
||||||
|
return {
|
||||||
|
tool: {
|
||||||
|
"paren-repair": parenRepairTool,
|
||||||
|
"penpot-psql": penpotPsqlTool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// import { tool } from "@opencode-ai/plugin"
|
||||||
|
// import path from "path"
|
||||||
|
// import { spawn } from "child_process"
|
||||||
|
|
||||||
|
// function formatFiles(files) {
|
||||||
|
// if (files.length === 0) return "stdin"
|
||||||
|
|
||||||
|
// // Keep the visible tool title reasonably short.
|
||||||
|
// if (files.length <= 3) return files.join(", ")
|
||||||
|
|
||||||
|
// return `${files.slice(0, 3).join(", ")} (+${files.length - 3} more)`
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const parenRepairTool = tool({
|
||||||
|
// description:
|
||||||
|
// "Fix mismatched parentheses/braces in Clojure files, then reformat with cljfmt.",
|
||||||
|
|
||||||
|
// args: {
|
||||||
|
// files: tool.schema
|
||||||
|
// .array(tool.schema.string())
|
||||||
|
// .describe("Array of file paths to fix")
|
||||||
|
// .optional(),
|
||||||
|
|
||||||
|
// code: tool.schema
|
||||||
|
// .string()
|
||||||
|
// .describe("Code string to fix via stdin")
|
||||||
|
// .optional(),
|
||||||
|
// },
|
||||||
|
|
||||||
|
// async execute(args, context) {
|
||||||
|
// const script = path.join(context.worktree, "scripts/paren-repair")
|
||||||
|
|
||||||
|
// const files = (args.files ?? []).map((file) => {
|
||||||
|
// const absolute = path.isAbsolute(file)
|
||||||
|
// ? file
|
||||||
|
// : path.resolve(context.worktree, file)
|
||||||
|
|
||||||
|
// return path.relative(context.worktree, absolute)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const targetSummary =
|
||||||
|
// files.length > 0
|
||||||
|
// ? formatFiles(files)
|
||||||
|
// : args.code !== undefined
|
||||||
|
// ? `stdin (${args.code.length} chars)`
|
||||||
|
// : "no input"
|
||||||
|
|
||||||
|
// // This updates the tool-call title immediately, while it is running.
|
||||||
|
// await context.metadata({
|
||||||
|
// title: `Paren repair: ${targetSummary}`,
|
||||||
|
// metadata: {
|
||||||
|
// files,
|
||||||
|
// codeChars: args.code?.length,
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const childArgs =
|
||||||
|
// args.files && args.files.length > 0
|
||||||
|
// ? [script, ...args.files]
|
||||||
|
// : [script]
|
||||||
|
|
||||||
|
// return new Promise((resolve) => {
|
||||||
|
// const proc = spawn("bb", childArgs, {
|
||||||
|
// cwd: context.worktree,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// let stdout = ""
|
||||||
|
// let stderr = ""
|
||||||
|
|
||||||
|
// if (args.code !== undefined) {
|
||||||
|
// proc.stdin.end(args.code)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// proc.stdout.on("data", (data) => {
|
||||||
|
// stdout += data.toString()
|
||||||
|
// })
|
||||||
|
|
||||||
|
// proc.stderr.on("data", (data) => {
|
||||||
|
// stderr += data.toString()
|
||||||
|
// })
|
||||||
|
|
||||||
|
// proc.on("close", (exitCode) => {
|
||||||
|
// const successful = exitCode === 0
|
||||||
|
|
||||||
|
// const commandOutput = successful
|
||||||
|
// ? stdout.trim() || "No changes needed"
|
||||||
|
// : `Error (exit ${exitCode}): ${(stderr || stdout).trim()}`
|
||||||
|
|
||||||
|
// const parameterOutput =
|
||||||
|
// files.length > 0
|
||||||
|
// ? `Files passed:\n${files.map((file) => `- ${file}`).join("\n")}`
|
||||||
|
// : args.code !== undefined
|
||||||
|
// ? `Input passed through stdin: ${args.code.length} characters`
|
||||||
|
// : "No files or stdin input were passed"
|
||||||
|
|
||||||
|
// resolve({
|
||||||
|
// title: `Paren repair: ${targetSummary}`,
|
||||||
|
// output: `${parameterOutput}\n\n${commandOutput}`,
|
||||||
|
// metadata: {
|
||||||
|
// files,
|
||||||
|
// codeChars: args.code?.length,
|
||||||
|
// exitCode,
|
||||||
|
// successful,
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// proc.on("error", (error) => {
|
||||||
|
// resolve({
|
||||||
|
// title: `Paren repair failed: ${targetSummary}`,
|
||||||
|
// output: [
|
||||||
|
// files.length > 0
|
||||||
|
// ? `Files passed:\n${files.map((file) => `- ${file}`).join("\n")}`
|
||||||
|
// : `Input: ${targetSummary}`,
|
||||||
|
// `Failed to start bb: ${error.message}`,
|
||||||
|
// ].join("\n\n"),
|
||||||
|
// metadata: {
|
||||||
|
// files,
|
||||||
|
// codeChars: args.code?.length,
|
||||||
|
// successful: false,
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
|
// export default async function plugin() {
|
||||||
|
// return {
|
||||||
|
// tool: {
|
||||||
|
// "paren-repair": parenRepairTool,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@ -1,85 +0,0 @@
|
|||||||
---
|
|
||||||
name: backport-commit
|
|
||||||
description: Port changes from a specific Git commit to the current branch by manually applying the diff, avoiding cherry-pick when it would introduce complex conflicts.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Backport Commit
|
|
||||||
|
|
||||||
Port changes from a specific Git commit to the current branch by manually
|
|
||||||
applying the diff, avoiding `git cherry-pick` when it would introduce
|
|
||||||
complex conflicts.
|
|
||||||
|
|
||||||
## When to Use
|
|
||||||
|
|
||||||
Use this skill whenever the user asks to backport a commit, especially when:
|
|
||||||
|
|
||||||
- The commit touches multiple modules or files with significant divergence
|
|
||||||
- `git cherry-pick` is explicitly ruled out ("do not use cherry-pick")
|
|
||||||
- The target commit is old enough that conflicts are likely
|
|
||||||
- The commit introduces both source changes AND new files (tests, etc.)
|
|
||||||
- You need full control over how each hunk is applied
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
### 1. Identify the target commit
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verify the commit exists and understand what it does
|
|
||||||
git log --oneline -1 <commit-sha>
|
|
||||||
|
|
||||||
# Get the full diff (including new/deleted files)
|
|
||||||
git show <commit-sha>
|
|
||||||
|
|
||||||
# Capture the original commit message for later reuse
|
|
||||||
git log --format='%B' -1 <commit-sha>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Identify affected modules
|
|
||||||
|
|
||||||
From the file paths in the diff, determine which Penpot modules are affected
|
|
||||||
(frontend, backend, common, render-wasm, etc.) and read their `AGENTS.md`
|
|
||||||
files **before** making any changes. If a module has no `AGENTS.md`, skip
|
|
||||||
that step — verify with `ls <module>/AGENTS.md` first.
|
|
||||||
|
|
||||||
### 3. Read the current state of each affected file
|
|
||||||
|
|
||||||
For every file the diff touches, read the current version on disk to understand
|
|
||||||
context and ensure correct placement before editing.
|
|
||||||
|
|
||||||
### 4. Apply changes manually (the core of this approach)
|
|
||||||
|
|
||||||
Process every hunk in the diff using the appropriate tool:
|
|
||||||
|
|
||||||
| Diff action | Tool to use |
|
|
||||||
|-------------|-------------|
|
|
||||||
| Modify existing file | `edit` — use enough surrounding context in `oldString` to uniquely match the location |
|
|
||||||
| Add new file | `write` — include proper license header and namespace conventions matching project style |
|
|
||||||
| Delete file | `bash rm <path>` |
|
|
||||||
| Rename/move file | `bash mv <old> <new>`, then apply any content changes with `edit` |
|
|
||||||
|
|
||||||
> **Tip:** Group nearby hunks from the same file into a single `edit` call.
|
|
||||||
> Use separate calls when hunks are far apart to keep `oldString` short and
|
|
||||||
> unambiguous.
|
|
||||||
|
|
||||||
Repeat until **all** hunks in the diff are ported.
|
|
||||||
|
|
||||||
### 5. Validate
|
|
||||||
|
|
||||||
Run **lint**, **check-fmt**, and **tests** for every affected module (see each
|
|
||||||
module's `AGENTS.md` for the exact commands). If the formatter auto-fixes
|
|
||||||
indentation, verify the logic is still semantically correct. All checks must
|
|
||||||
pass before moving on.
|
|
||||||
|
|
||||||
### 6. Commit
|
|
||||||
|
|
||||||
Ask the `commiter` sub-agent to create a commit. Stage all relevant files
|
|
||||||
(exclude unrelated untracked files) and provide the original commit message as
|
|
||||||
a reference, adapting it as needed for the target branch context.
|
|
||||||
|
|
||||||
## Key Principles
|
|
||||||
|
|
||||||
- **Context matters** — always read files before editing; never guess
|
|
||||||
indentation or surrounding code
|
|
||||||
- **Lint + format + test** — never skip validation before committing
|
|
||||||
- **Preserve intent** — keep the original commit message meaning; the
|
|
||||||
`commiter` agent handles formatting
|
|
||||||
@ -1,397 +0,0 @@
|
|||||||
---
|
|
||||||
name: code-review-and-quality
|
|
||||||
description: Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to assess code quality across multiple dimensions before it enters the main branch.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Code Review and Quality
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Multi-dimensional code review with quality gates. Every change gets reviewed before merge — no exceptions. Review covers five axes: correctness, readability, architecture, security, and performance.
|
|
||||||
|
|
||||||
**The approval standard:** Approve a change when it definitely improves overall code health, even if it isn't perfect. Perfect code doesn't exist — the goal is continuous improvement. Don't block a change because it isn't exactly how you would have written it. If it improves the codebase and follows the project's conventions, approve it.
|
|
||||||
|
|
||||||
## When to Use
|
|
||||||
|
|
||||||
- Before merging any PR or change
|
|
||||||
- After completing a feature implementation
|
|
||||||
- When another agent or model produced code you need to evaluate
|
|
||||||
- When refactoring existing code
|
|
||||||
- After any bug fix (review both the fix and the regression test)
|
|
||||||
|
|
||||||
## The Five-Axis Review
|
|
||||||
|
|
||||||
Every review evaluates code across these dimensions:
|
|
||||||
|
|
||||||
### 1. Correctness
|
|
||||||
|
|
||||||
Does the code do what it claims to do?
|
|
||||||
|
|
||||||
- Does it match the spec or task requirements?
|
|
||||||
- Are edge cases handled (null, empty, boundary values)?
|
|
||||||
- Are error paths handled (not just the happy path)?
|
|
||||||
- Does it pass all tests? Are the tests actually testing the right things?
|
|
||||||
- Are there off-by-one errors, race conditions, or state inconsistencies?
|
|
||||||
|
|
||||||
### 2. Readability & Simplicity
|
|
||||||
|
|
||||||
Can another engineer (or agent) understand this code without the author explaining it?
|
|
||||||
|
|
||||||
- Are names descriptive and consistent with project conventions? (No `temp`, `data`, `result` without context)
|
|
||||||
- Is the control flow straightforward (avoid nested ternaries, deep callbacks)?
|
|
||||||
- Is the code organized logically (related code grouped, clear module boundaries)?
|
|
||||||
- Are there any "clever" tricks that should be simplified?
|
|
||||||
- **Could this be done in fewer lines?** (1000 lines where 100 suffice is a failure)
|
|
||||||
- **Are abstractions earning their complexity?** (Don't generalize until the third use case)
|
|
||||||
- Would comments help clarify non-obvious intent? (But don't comment obvious code.)
|
|
||||||
- Are there dead code artifacts: no-op variables (`_unused`), backwards-compat shims, or `// removed` comments?
|
|
||||||
- **Is a new conditional bolted onto an unrelated flow?** That's a design smell, not a nit — push the logic into its own helper, state, or policy instead of tangling an existing path.
|
|
||||||
- **Do repeated conditionals on the same shape appear?** They signal a missing model or dispatcher. A "temporary" branch is usually permanent debt.
|
|
||||||
|
|
||||||
### 3. Architecture
|
|
||||||
|
|
||||||
Does the change fit the system's design?
|
|
||||||
|
|
||||||
- Does it follow existing patterns or introduce a new one? If new, is it justified?
|
|
||||||
- Does it maintain clean module boundaries?
|
|
||||||
- Is there code duplication that should be shared?
|
|
||||||
- Are dependencies flowing in the right direction (no circular dependencies)?
|
|
||||||
- Is the abstraction level appropriate (not over-engineered, not too coupled)?
|
|
||||||
- **Does this refactor reduce complexity or just relocate it?** Count the concepts a reader must hold to follow the change. If a "cleaner" version leaves that count unchanged, it isn't cleaner — prefer the restructuring that makes whole branches, modes, or layers disappear over one that re-centralizes the same logic. Prefer deleting an abstraction to polishing it.
|
|
||||||
- **Is feature-specific logic leaking into a shared or general-purpose module?** Keep logic in its owning layer, reuse the existing canonical helper instead of a near-duplicate, and don't normalize architectural drift.
|
|
||||||
- **Are type boundaries explicit?** Question gratuitous `any`/`unknown`/optional/casts and silent fallbacks that paper over an unclear invariant — making the boundary explicit often makes the surrounding control flow simpler.
|
|
||||||
|
|
||||||
### 4. Security
|
|
||||||
|
|
||||||
For detailed security guidance, see `security-and-hardening`. Does the change introduce vulnerabilities?
|
|
||||||
|
|
||||||
- Is user input validated and sanitized?
|
|
||||||
- Are secrets kept out of code, logs, and version control?
|
|
||||||
- Is authentication/authorization checked where needed?
|
|
||||||
- Are SQL queries parameterized (no string concatenation)?
|
|
||||||
- Are outputs encoded to prevent XSS?
|
|
||||||
- Are dependencies from trusted sources with no known vulnerabilities?
|
|
||||||
- Is data from external sources (APIs, logs, user content, config files) treated as untrusted?
|
|
||||||
- Are external data flows validated at system boundaries before use in logic or rendering?
|
|
||||||
|
|
||||||
### 5. Performance
|
|
||||||
|
|
||||||
Does the change introduce performance problems?
|
|
||||||
|
|
||||||
- Any N+1 query patterns?
|
|
||||||
- Any unbounded loops or unconstrained data fetching?
|
|
||||||
- Any synchronous operations that should be async?
|
|
||||||
- Any unnecessary re-renders in UI components?
|
|
||||||
- Any missing pagination on list endpoints?
|
|
||||||
- Any large objects created in hot paths?
|
|
||||||
|
|
||||||
## Structural Remedies
|
|
||||||
|
|
||||||
When you flag a structural problem, propose the move — not just the problem. A review that only says "this is complex" leaves the author guessing. Reach for a named restructuring:
|
|
||||||
|
|
||||||
- **Replace a chain of conditionals** with a typed model or an explicit dispatcher.
|
|
||||||
- **Collapse duplicate branches** into a single clearer flow.
|
|
||||||
- **Separate orchestration from business logic** so each reads on its own.
|
|
||||||
- **Move feature-specific logic** out of a shared module into the package that owns the concept.
|
|
||||||
- **Reuse the canonical helper** instead of a bespoke near-duplicate.
|
|
||||||
- **Make a type boundary explicit** so downstream branching disappears.
|
|
||||||
- **Delete a pass-through wrapper** that adds indirection without clarifying the API.
|
|
||||||
- **Extract a helper, or split a large file** into focused modules.
|
|
||||||
|
|
||||||
Prefer the remedy that removes moving pieces over one that spreads the same complexity around.
|
|
||||||
|
|
||||||
## Change Sizing
|
|
||||||
|
|
||||||
Small, focused changes are easier to review, faster to merge, and safer to deploy. Target these sizes:
|
|
||||||
|
|
||||||
```
|
|
||||||
~100 lines changed → Good. Reviewable in one sitting.
|
|
||||||
~300 lines changed → Acceptable if it's a single logical change.
|
|
||||||
~1000 lines changed → Too large. Split it.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Watch file size, not just diff size.** A small diff can still push a file past a healthy boundary — around 1000 *total* lines in a single file (distinct from the ~1000 *changed*-lines threshold above) is a common inspection signal, not a hard cap. When a change materially grows an already-large file, ask whether to extract helpers, subcomponents, or modules *first*, before piling more on. Decompose, then add.
|
|
||||||
|
|
||||||
**What counts as "one change":** A single self-contained modification that addresses one thing, includes related tests, and keeps the system functional after submission. One part of a feature — not the whole feature.
|
|
||||||
|
|
||||||
**Splitting strategies when a change is too large:**
|
|
||||||
|
|
||||||
| Strategy | How | When |
|
|
||||||
|----------|-----|------|
|
|
||||||
| **Stack** | Submit a small change, start the next one based on it | Sequential dependencies |
|
|
||||||
| **By file group** | Separate changes for groups needing different reviewers | Cross-cutting concerns |
|
|
||||||
| **Horizontal** | Create shared code/stubs first, then consumers | Layered architecture |
|
|
||||||
| **Vertical** | Break into smaller full-stack slices of the feature | Feature work |
|
|
||||||
|
|
||||||
**When large changes are acceptable:** Complete file deletions and automated refactoring where the reviewer only needs to verify intent, not every line.
|
|
||||||
|
|
||||||
**Separate refactoring from feature work.** A change that refactors existing code and adds new behavior is two changes — submit them separately. Small cleanups (variable renaming) can be included at reviewer discretion.
|
|
||||||
|
|
||||||
## Change Descriptions
|
|
||||||
|
|
||||||
Every change needs a description that stands alone in version control history.
|
|
||||||
|
|
||||||
**First line:** Short, imperative, standalone. "Delete the FizzBuzz RPC" not "Deleting the FizzBuzz RPC." Must be informative enough that someone searching history can understand the change without reading the diff.
|
|
||||||
|
|
||||||
**Body:** What is changing and why. Include context, decisions, and reasoning not visible in the code itself. Link to bug numbers, benchmark results, or design docs where relevant. Acknowledge approach shortcomings when they exist.
|
|
||||||
|
|
||||||
**Anti-patterns:** "Fix bug," "Fix build," "Add patch," "Moving code from A to B," "Phase 1," "Add convenience functions."
|
|
||||||
|
|
||||||
## Review Process
|
|
||||||
|
|
||||||
### Step 1: Understand the Context
|
|
||||||
|
|
||||||
Before looking at code, understand the intent:
|
|
||||||
|
|
||||||
```
|
|
||||||
- What is this change trying to accomplish?
|
|
||||||
- What spec or task does it implement?
|
|
||||||
- What is the expected behavior change?
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Review the Tests First
|
|
||||||
|
|
||||||
Tests reveal intent and coverage:
|
|
||||||
|
|
||||||
```
|
|
||||||
- Do tests exist for the change?
|
|
||||||
- Do they test behavior (not implementation details)?
|
|
||||||
- Are edge cases covered?
|
|
||||||
- Do tests have descriptive names?
|
|
||||||
- Would the tests catch a regression if the code changed?
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Review the Implementation
|
|
||||||
|
|
||||||
Walk through the code with the five axes in mind:
|
|
||||||
|
|
||||||
```
|
|
||||||
For each file changed:
|
|
||||||
1. Correctness: Does this code do what the test says it should?
|
|
||||||
2. Readability: Can I understand this without help?
|
|
||||||
3. Architecture: Does this fit the system?
|
|
||||||
4. Security: Any vulnerabilities?
|
|
||||||
5. Performance: Any bottlenecks?
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Categorize Findings
|
|
||||||
|
|
||||||
Label every comment with its severity so the author knows what's required vs optional:
|
|
||||||
|
|
||||||
| Prefix | Meaning | Author Action |
|
|
||||||
|--------|---------|---------------|
|
|
||||||
| *(no prefix)* | Required change | Must address before merge |
|
|
||||||
| **Critical:** | Blocks merge | Security vulnerability, data loss, broken functionality |
|
|
||||||
| **Nit:** | Minor, optional | Author may ignore — formatting, style preferences |
|
|
||||||
| **Optional:** / **Consider:** | Suggestion | Worth considering but not required |
|
|
||||||
| **FYI** | Informational only | No action needed — context for future reference |
|
|
||||||
|
|
||||||
This prevents authors from treating all feedback as mandatory and wasting time on optional suggestions.
|
|
||||||
|
|
||||||
**Lead with what matters.** Order findings by leverage: correctness and security first, then structural regressions and missed simplifications, then everything else. Don't bury a real issue under cosmetic nits — a few high-conviction comments beat a long list. If you have one structural problem and ten nits, the structural problem *is* the review.
|
|
||||||
|
|
||||||
### Step 5: Verify the Verification
|
|
||||||
|
|
||||||
Check the author's verification story:
|
|
||||||
|
|
||||||
```
|
|
||||||
- What tests were run?
|
|
||||||
- Did the build pass?
|
|
||||||
- Was the change tested manually?
|
|
||||||
- Are there screenshots for UI changes?
|
|
||||||
- Is there a before/after comparison?
|
|
||||||
```
|
|
||||||
|
|
||||||
## Multi-Model Review Pattern
|
|
||||||
|
|
||||||
Use different models for different review perspectives:
|
|
||||||
|
|
||||||
```
|
|
||||||
Model A writes the code
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Model B reviews for correctness and architecture
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Model A addresses the feedback
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Human makes the final call
|
|
||||||
```
|
|
||||||
|
|
||||||
This catches issues that a single model might miss — different models have different blind spots.
|
|
||||||
|
|
||||||
**Example prompt for a review agent:**
|
|
||||||
```
|
|
||||||
Review this code change for correctness, security, and adherence to
|
|
||||||
our project conventions. The spec says [X]. The change should [Y].
|
|
||||||
Flag any issues as Critical, Required, Optional, or Nit.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dead Code Hygiene
|
|
||||||
|
|
||||||
After any refactoring or implementation change, check for orphaned code:
|
|
||||||
|
|
||||||
1. Identify code that is now unreachable or unused
|
|
||||||
2. List it explicitly
|
|
||||||
3. **Ask before deleting:** "Should I remove these now-unused elements: [list]?"
|
|
||||||
|
|
||||||
Don't leave dead code lying around — it confuses future readers and agents. But don't silently delete things you're not sure about. When in doubt, ask.
|
|
||||||
|
|
||||||
```
|
|
||||||
DEAD CODE IDENTIFIED:
|
|
||||||
- formatLegacyDate() in src/utils/date.ts — replaced by formatDate()
|
|
||||||
- OldTaskCard component in src/components/ — replaced by TaskCard
|
|
||||||
- LEGACY_API_URL constant in src/config.ts — no remaining references
|
|
||||||
→ Safe to remove these?
|
|
||||||
```
|
|
||||||
|
|
||||||
## Review Speed
|
|
||||||
|
|
||||||
Slow reviews block entire teams. The cost of context-switching to review is less than the waiting cost imposed on others.
|
|
||||||
|
|
||||||
- **Respond within one business day** — this is the maximum, not the target
|
|
||||||
- **Ideal cadence:** Respond shortly after a review request arrives, unless deep in focused coding. A typical change should complete multiple review rounds in a single day
|
|
||||||
- **Prioritize fast individual responses** over quick final approval. Quick feedback reduces frustration even if multiple rounds are needed
|
|
||||||
- **Large changes:** Ask the author to split them rather than reviewing one massive changeset
|
|
||||||
|
|
||||||
## Handling Disagreements
|
|
||||||
|
|
||||||
When resolving review disputes, apply this hierarchy:
|
|
||||||
|
|
||||||
1. **Technical facts and data** override opinions and preferences
|
|
||||||
2. **Style guides** are the absolute authority on style matters
|
|
||||||
3. **Software design** must be evaluated on engineering principles, not personal preference
|
|
||||||
4. **Codebase consistency** is acceptable if it doesn't degrade overall health
|
|
||||||
|
|
||||||
**Don't accept "I'll clean it up later."** Experience shows deferred cleanup rarely happens. Require cleanup before submission unless it's a genuine emergency. If surrounding issues can't be addressed in this change, require filing a bug with self-assignment.
|
|
||||||
|
|
||||||
## Honesty in Review
|
|
||||||
|
|
||||||
When reviewing code — whether written by you, another agent, or a human:
|
|
||||||
|
|
||||||
- **Don't rubber-stamp.** "LGTM" without evidence of review helps no one.
|
|
||||||
- **Don't soften real issues.** "This might be a minor concern" when it's a bug that will hit production is dishonest.
|
|
||||||
- **Quantify problems when possible.** "This N+1 query will add ~50ms per item in the list" is better than "this could be slow."
|
|
||||||
- **Push back on approaches with clear problems.** Sycophancy is a failure mode in reviews. If the implementation has issues, say so directly and propose alternatives.
|
|
||||||
- **Accept override gracefully.** If the author has full context and disagrees, defer to their judgment. Comment on code, not people — reframe personal critiques to focus on the code itself.
|
|
||||||
|
|
||||||
## Dependency Discipline
|
|
||||||
|
|
||||||
Part of code review is dependency review:
|
|
||||||
|
|
||||||
**Before adding any dependency:**
|
|
||||||
|
|
||||||
1. Does the existing stack solve this? (Often it does.)
|
|
||||||
2. How large is the dependency? (Check bundle impact.)
|
|
||||||
3. Is it actively maintained? (Check last commit, open issues.)
|
|
||||||
4. Does it have known vulnerabilities? (`npm audit`)
|
|
||||||
5. What's the license? (Must be compatible with the project.)
|
|
||||||
|
|
||||||
**Rule:** Prefer standard library and existing utilities over new dependencies. Every dependency is a liability.
|
|
||||||
|
|
||||||
**Upgrading an existing dependency** is a code change like any other, and the riskiest upgrades are the ones merged in bulk with a message like "bump deps." Review them with the same discipline:
|
|
||||||
|
|
||||||
1. **Read the changelog, not just the version number.** Semver is a promise the maintainer may not have kept — a "patch" can carry a behavioral change. For a major bump, read the migration notes and find what breaks.
|
|
||||||
2. **One dependency per change.** Upgrade and merge them individually (or in small related groups). When a bulk bump breaks the build, you've lost which package did it; a single-package change makes the cause obvious and the revert clean.
|
|
||||||
3. **Let the tests decide.** The upgrade is verified by a green suite before *and* after, not by "it installed." If coverage around the dependency's behavior is thin, that gap is the real finding — add a test first.
|
|
||||||
4. **Mind the transitive graph.** Most installed packages are ones nobody chose directly. Review the lockfile diff, not just `package.json`; a single direct bump can pull in dozens of indirect changes.
|
|
||||||
5. **Keep the lockfile honest.** Commit it, review its diff, and never hand-edit it. The lockfile is the thing that actually pins what ships.
|
|
||||||
|
|
||||||
For triaging `npm audit` findings and supply-chain risk (typosquatting, compromised maintainers), follow the `security-and-hardening` skill — this section covers the upgrade *workflow*, that one covers the security verdict.
|
|
||||||
|
|
||||||
## The Review Checklist
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## Review: [PR/Change title]
|
|
||||||
|
|
||||||
### Context
|
|
||||||
- [ ] I understand what this change does and why
|
|
||||||
|
|
||||||
### Correctness
|
|
||||||
- [ ] Change matches spec/task requirements
|
|
||||||
- [ ] Edge cases handled
|
|
||||||
- [ ] Error paths handled
|
|
||||||
- [ ] Tests cover the change adequately
|
|
||||||
|
|
||||||
### Readability
|
|
||||||
- [ ] Names are clear and consistent
|
|
||||||
- [ ] Logic is straightforward
|
|
||||||
- [ ] No unnecessary complexity
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
- [ ] Follows existing patterns
|
|
||||||
- [ ] No unnecessary coupling or dependencies
|
|
||||||
- [ ] Appropriate abstraction level
|
|
||||||
- [ ] Refactors reduce complexity rather than relocate it
|
|
||||||
- [ ] No feature logic in shared modules; file stays within a healthy size
|
|
||||||
|
|
||||||
### Security
|
|
||||||
- [ ] No secrets in code
|
|
||||||
- [ ] Input validated at boundaries
|
|
||||||
- [ ] No injection vulnerabilities
|
|
||||||
- [ ] Auth checks in place
|
|
||||||
- [ ] External data sources treated as untrusted
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
- [ ] No N+1 patterns
|
|
||||||
- [ ] No unbounded operations
|
|
||||||
- [ ] Pagination on list endpoints
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
- [ ] Tests pass
|
|
||||||
- [ ] Build succeeds
|
|
||||||
- [ ] Manual verification done (if applicable)
|
|
||||||
|
|
||||||
### Verdict
|
|
||||||
- [ ] **Approve** — Ready to merge
|
|
||||||
- [ ] **Request changes** — Issues must be addressed
|
|
||||||
```
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- For detailed security review guidance, see `security-and-hardening`
|
|
||||||
|
|
||||||
## Common Rationalizations
|
|
||||||
|
|
||||||
| Rationalization | Reality |
|
|
||||||
|---|---|
|
|
||||||
| "It works, that's good enough" | Working code that's unreadable, insecure, or architecturally wrong creates debt that compounds. |
|
|
||||||
| "I wrote it, so I know it's correct" | Authors are blind to their own assumptions. Every change benefits from another set of eyes. |
|
|
||||||
| "We'll clean it up later" | Later never comes. The review is the quality gate — use it. Require cleanup before merge, not after. |
|
|
||||||
| "AI-generated code is probably fine" | AI code needs more scrutiny, not less. It's confident and plausible, even when wrong. |
|
|
||||||
| "The tests pass, so it's good" | Tests are necessary but not sufficient. They don't catch architecture problems, security issues, or readability concerns. |
|
|
||||||
| "The refactor makes it cleaner" | Relocating complexity isn't reducing it. If the reader still holds the same number of concepts, the structure didn't improve — look for the version where branches disappear. |
|
|
||||||
| "It's only a small addition to this file" | Small diffs still push files past a healthy size and bolt branches onto unrelated flows. Judge the resulting structure, not the diff size. |
|
|
||||||
| "It's just a version bump" | A bump is a behavior change you didn't write. Read the changelog; semver doesn't guarantee no breakage. |
|
|
||||||
| "I'll upgrade everything in one PR to save time" | A bulk bump that breaks the build hides which package did it. One dependency per change keeps the cause and the revert clean. |
|
|
||||||
|
|
||||||
## Red Flags
|
|
||||||
|
|
||||||
- PRs merged without any review
|
|
||||||
- Review that only checks if tests pass (ignoring other axes)
|
|
||||||
- "LGTM" without evidence of actual review
|
|
||||||
- Security-sensitive changes without security-focused review
|
|
||||||
- Large PRs that are "too big to review properly" (split them)
|
|
||||||
- No regression tests with bug fix PRs
|
|
||||||
- Review comments without severity labels — makes it unclear what's required vs optional
|
|
||||||
- Accepting "I'll fix it later" — it never happens
|
|
||||||
- A refactor that moves code around without reducing the number of concepts a reader must hold
|
|
||||||
- A change that grows an already-large file instead of decomposing it
|
|
||||||
- New conditionals scattered into unrelated code paths (a missing abstraction)
|
|
||||||
- A bespoke helper that duplicates an existing canonical one, or feature logic placed in a shared module
|
|
||||||
- A bulk "bump dependencies" PR with no changelog review and no per-package isolation
|
|
||||||
- A lockfile change that's hand-edited, uncommitted, or merged without reviewing its diff
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
After review is complete:
|
|
||||||
|
|
||||||
- [ ] All Critical issues are resolved
|
|
||||||
- [ ] All Required (no-prefix) changes are resolved or explicitly deferred with justification
|
|
||||||
- [ ] Tests pass
|
|
||||||
- [ ] Build succeeds
|
|
||||||
- [ ] The verification story is documented (what changed, how it was verified)
|
|
||||||
- [ ] Dependency upgrades were reviewed against their changelog, isolated per package, and verified by a green suite with the lockfile diff reviewed
|
|
||||||
|
|
||||||
**Presumptive blockers:** surface and propose the simpler design for each of these; escalate to Required only when the change actively makes structure worse: a refactor that relocates complexity instead of reducing it; a change that pushes a file past the size boundary with no decomposition; feature logic added to a shared module; a near-duplicate of an existing canonical helper; a silent fallback that hides an unclear invariant.
|
|
||||||
255
.opencode/skills/code-review/SKILL.md
Normal file
255
.opencode/skills/code-review/SKILL.md
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
---
|
||||||
|
name: code-review
|
||||||
|
description: Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to assess code quality across multiple dimensions before it enters the main branch.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code Review and Quality
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Multi-dimensional code review with quality gates. Every change gets reviewed before merge — no exceptions. Review covers five axes: correctness, readability, architecture, security, and performance.
|
||||||
|
|
||||||
|
**The approval standard:** Approve a change when it definitely improves overall code health, even if it isn't perfect. Perfect code doesn't exist — the goal is continuous improvement. Don't block a change because it isn't exactly how you would have written it. If it improves the codebase and follows the project's conventions, approve it.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Before merging any PR or change
|
||||||
|
- After completing a feature implementation
|
||||||
|
- When another agent or model produced code you need to evaluate
|
||||||
|
- When refactoring existing code
|
||||||
|
- After any bug fix (review both the fix and the regression test)
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
These principles underpin every axis. When in doubt, default to them.
|
||||||
|
|
||||||
|
- **DRY (Don't Repeat Yourself):** Every piece of knowledge has one authoritative representation. If the same logic appears in two places, extract it into a shared helper, model, or type. Reviewers: flag duplicated logic as a required change — it's not "just similar," it's drift that will diverge.
|
||||||
|
- **KISS (Keep It Simple, Stupid):** The simplest solution that works is the best solution. Complexity must earn its place. Reviewers: if you need more than one sentence to explain what a piece of code does, it's too complex — push for simplification before merge.
|
||||||
|
- **YAGNI (You Aren't Gonna Need It):** Don't add abstractions, hooks, or generalizations for hypothetical future use cases. Generalize on the third occurrence, not the first. Reviewers: delete speculative generality.
|
||||||
|
- **Don't invent problems:** Do not manufacture issues to produce more feedback. Every finding must be a real risk, a real readability barrier, or a real architectural concern — not a hypothetical or a stylistic preference disguised as a problem.
|
||||||
|
|
||||||
|
## The Five-Axis Review
|
||||||
|
|
||||||
|
Every review evaluates code across these dimensions.
|
||||||
|
|
||||||
|
### 1. Correctness
|
||||||
|
|
||||||
|
Does the code do what it claims to do?
|
||||||
|
|
||||||
|
- Does it match the spec or task requirements?
|
||||||
|
- Are edge cases handled (null, empty, boundary values)?
|
||||||
|
- Are error paths handled (not just the happy path)?
|
||||||
|
- Does it pass all tests? Are the tests actually testing the right things?
|
||||||
|
- Are there off-by-one errors, race conditions, or state inconsistencies?
|
||||||
|
|
||||||
|
### 2. Readability & Simplicity
|
||||||
|
|
||||||
|
Can another engineer (or agent) understand this code without the author explaining it?
|
||||||
|
|
||||||
|
- Are names descriptive and consistent with project conventions? (No `temp`, `data`, `result` without context)
|
||||||
|
- Is the control flow straightforward (avoid nested ternaries, deep callbacks)?
|
||||||
|
- Are there any "clever" tricks that should be simplified?
|
||||||
|
- **KISS check:** Is this the simplest approach that solves the problem? A 20-line straightforward function beats a 5-line clever one that requires a comment to explain.
|
||||||
|
- Could this be done in fewer lines? (1000 lines where 100 suffice is a failure)
|
||||||
|
- Are abstractions earning their complexity? (Don't generalize until the third use case)
|
||||||
|
- Is a new conditional bolted onto an unrelated flow? Push the logic into its own helper, state, or policy.
|
||||||
|
- Do repeated conditionals on the same shape appear? They signal a missing model or dispatcher.
|
||||||
|
- Are there dead code artifacts: no-op variables, backwards-compat shims, or `// removed` comments?
|
||||||
|
|
||||||
|
### 3. Architecture
|
||||||
|
|
||||||
|
Does the change fit the system's design?
|
||||||
|
|
||||||
|
- Does it follow existing patterns or introduce a new one? If new, is it justified?
|
||||||
|
- Does it maintain clean module boundaries?
|
||||||
|
- **DRY check:** Is there existing code that does the same thing? Reuse the canonical helper instead of writing a near-duplicate. If two branches do nearly the same thing, collapse them.
|
||||||
|
- Are dependencies flowing in the right direction (no circular dependencies)?
|
||||||
|
- Is the abstraction level appropriate (not over-engineered, not too coupled)?
|
||||||
|
- Does this refactor reduce complexity or just relocate it? Count the concepts a reader must hold. Prefer the restructuring that makes whole branches disappear over one that re-centralizes the same logic. Prefer deleting an abstraction to polishing it.
|
||||||
|
- Is feature-specific logic leaking into a shared or general-purpose module?
|
||||||
|
- Are type boundaries explicit? Question gratuitous `any`/`unknown`/optional/casts and silent fallbacks.
|
||||||
|
- **Structural remedies:** When you flag a problem, propose the move — not just the problem. Replace conditionals with dispatchers, collapse duplicate branches, separate orchestration from business logic, extract helpers, split large files. Prefer the remedy that removes moving pieces over one that spreads the same complexity around.
|
||||||
|
|
||||||
|
### 4. Security
|
||||||
|
|
||||||
|
For detailed security guidance, see `security-and-hardening`.
|
||||||
|
|
||||||
|
- Is user input validated and sanitized?
|
||||||
|
- Are secrets kept out of code, logs, and version control?
|
||||||
|
- Is authentication/authorization checked where needed?
|
||||||
|
- Are SQL queries parameterized (no string concatenation)?
|
||||||
|
- Are outputs encoded to prevent XSS?
|
||||||
|
- Are dependencies from trusted sources with no known vulnerabilities?
|
||||||
|
- Is data from external sources (APIs, logs, user content, config files) treated as untrusted?
|
||||||
|
|
||||||
|
### 5. Performance
|
||||||
|
|
||||||
|
- Any N+1 query patterns?
|
||||||
|
- Any unbounded loops or unconstrained data fetching?
|
||||||
|
- Any synchronous operations that should be async?
|
||||||
|
- Any unnecessary re-renders in UI components?
|
||||||
|
- Any missing pagination on list endpoints?
|
||||||
|
- Any large objects created in hot paths?
|
||||||
|
|
||||||
|
## Review Process
|
||||||
|
|
||||||
|
1. **Understand the intent** — What is this change trying to accomplish? What spec or task does it implement?
|
||||||
|
2. **Review tests first** — Tests reveal intent and coverage. Do they test behavior, not implementation details? Are edge cases covered?
|
||||||
|
3. **Review the implementation** — Walk through each file with the five axes in mind.
|
||||||
|
4. **Categorize findings** — Label every comment with its severity:
|
||||||
|
|
||||||
|
| Prefix | Meaning | Author Action |
|
||||||
|
|--------|---------|---------------|
|
||||||
|
| **Critical:** | Blocks merge | Security vulnerability, data loss, broken functionality |
|
||||||
|
| **High:** | Required change | Must address before merge |
|
||||||
|
| **Medium:** | Should fix | Strongly recommended, not a blocker |
|
||||||
|
| **Low:** | Minor, optional | Author may ignore — formatting, style preferences |
|
||||||
|
| **Suggestion:** | Worth considering | Not required, but improves the code |
|
||||||
|
|
||||||
|
For each finding, describe the circumstances under which it could fail: specific inputs, load conditions, timing, or user actions that trigger the problem. "This crashes when input is null" is actionable; "this might crash" is not.
|
||||||
|
|
||||||
|
Lead with what matters: correctness and security first, then structural issues, then everything else. A few high-conviction comments beat a long list.
|
||||||
|
|
||||||
|
5. **Verify the verification** — What tests were run? Did the build pass? Was the change tested manually? Screenshots for UI changes?
|
||||||
|
|
||||||
|
## Review Output
|
||||||
|
|
||||||
|
Structure every review using this format:
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Briefly explain what the code does and give an overall assessment.
|
||||||
|
|
||||||
|
### Critical and High-Priority Issues
|
||||||
|
|
||||||
|
List problems that could cause security incidents, data loss, crashes, incorrect behavior, or major performance degradation. For each: state the severity, identify the file/function/code section, explain why it's a problem, describe failure circumstances, and provide a concrete improvement with corrected code when useful.
|
||||||
|
|
||||||
|
### Other Findings
|
||||||
|
|
||||||
|
List medium- and low-priority issues, including maintainability and design concerns.
|
||||||
|
|
||||||
|
### Suggested Refactoring
|
||||||
|
|
||||||
|
Provide focused code changes or revised snippets. Preserve existing behavior unless a behavior change is explicitly justified.
|
||||||
|
|
||||||
|
### Testing Recommendations
|
||||||
|
|
||||||
|
Identify missing tests and describe specific test cases, including edge cases and failure scenarios.
|
||||||
|
|
||||||
|
### Positive Observations
|
||||||
|
|
||||||
|
Mention implementation choices that are clear, safe, efficient, or well designed. This is not fluff — it reinforces good patterns and tells the author what to keep doing.
|
||||||
|
|
||||||
|
### Final Verdict
|
||||||
|
|
||||||
|
Choose one:
|
||||||
|
|
||||||
|
- **Approve** — Ready to merge
|
||||||
|
- **Approve with minor changes** — Good to merge after addressing low/medium issues
|
||||||
|
- **Request changes** — Critical or high issues must be resolved before merge
|
||||||
|
|
||||||
|
## Change Sizing
|
||||||
|
|
||||||
|
Small, focused changes are easier to review, faster to merge, and safer to deploy.
|
||||||
|
|
||||||
|
```
|
||||||
|
~100 lines changed → Good. Reviewable in one sitting.
|
||||||
|
~300 lines changed → Acceptable if it's a single logical change.
|
||||||
|
~1000 lines changed → Too large. Split it.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Watch file size, not just diff size.** Around 1000 *total* lines in a single file is a common inspection signal. When a change materially grows an already-large file, decompose first.
|
||||||
|
|
||||||
|
**Splitting strategies:**
|
||||||
|
|
||||||
|
| Strategy | How | When |
|
||||||
|
|----------|-----|------|
|
||||||
|
| **Stack** | Submit a small change, start the next one based on it | Sequential dependencies |
|
||||||
|
| **By file group** | Separate changes for groups needing different reviewers | Cross-cutting concerns |
|
||||||
|
| **Horizontal** | Create shared code/stubs first, then consumers | Layered architecture |
|
||||||
|
| **Vertical** | Break into smaller full-stack slices of the feature | Feature work |
|
||||||
|
|
||||||
|
**Separate refactoring from feature work.** A change that refactors and adds new behavior is two changes — submit them separately.
|
||||||
|
|
||||||
|
## Change Descriptions
|
||||||
|
|
||||||
|
- **First line:** Short, imperative, standalone. "Delete the FizzBuzz RPC" not "Deleting the FizzBuzz RPC."
|
||||||
|
- **Body:** What is changing and why. Include context and reasoning not visible in the code itself.
|
||||||
|
- **Anti-patterns:** "Fix bug," "Fix build," "Add patch," "Phase 1."
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Before adding any dependency:
|
||||||
|
|
||||||
|
1. Does the existing stack solve this? (Often it does.)
|
||||||
|
2. How large is the dependency? (Check bundle impact.)
|
||||||
|
3. Is it actively maintained? (Check last commit, open issues.)
|
||||||
|
4. Does it have known vulnerabilities? (`npm audit`)
|
||||||
|
5. What's the license? (Must be compatible with the project.)
|
||||||
|
|
||||||
|
**Rule:** Prefer standard library and existing utilities over new dependencies. Every dependency is a liability.
|
||||||
|
|
||||||
|
**Upgrading dependencies:**
|
||||||
|
|
||||||
|
- Read the changelog, not just the version number. Semver is a promise the maintainer may not have kept.
|
||||||
|
- One dependency per change. When a bulk bump breaks the build, you've lost which package did it.
|
||||||
|
- Let the tests decide — a green suite before *and* after, not just "it installed."
|
||||||
|
- Review the lockfile diff, not just `package.json`. Commit it and never hand-edit it.
|
||||||
|
|
||||||
|
For supply-chain risk triage, follow the `security-and-hardening` skill.
|
||||||
|
|
||||||
|
## Common Rationalizations
|
||||||
|
|
||||||
|
| Rationalization | Reality |
|
||||||
|
|---|---|
|
||||||
|
| "It works, that's good enough" | Working code that's unreadable, insecure, or architecturally wrong creates debt that compounds. |
|
||||||
|
| "I wrote it, so I know it's correct" | Authors are blind to their own assumptions. Every change benefits from another set of eyes. |
|
||||||
|
| "We'll clean it up later" | Later never comes. The review is the quality gate — use it. |
|
||||||
|
| "AI-generated code is probably fine" | AI code needs more scrutiny, not less. It's confident and plausible, even when wrong. |
|
||||||
|
| "The tests pass, so it's good" | Tests are necessary but not sufficient. They don't catch architecture, security, or readability problems. |
|
||||||
|
| "The refactor makes it cleaner" | Relocating complexity isn't reducing it. If the reader still holds the same number of concepts, the structure didn't improve. |
|
||||||
|
| "It's only a small addition to this file" | Small diffs still push files past healthy size and bolt branches onto unrelated flows. |
|
||||||
|
| "It's just a version bump" | A bump is a behavior change you didn't write. Read the changelog. |
|
||||||
|
| "I'll upgrade everything in one PR" | A bulk bump hides which package broke the build. One per change. |
|
||||||
|
| "It's duplicated but it's only two places" | Two becomes three becomes five. Extract now, before the copies diverge. |
|
||||||
|
| "The abstraction is future-proof" | YAGNI. Delete speculative generality — generalize on the third occurrence, not the first. |
|
||||||
|
| "It's clever but efficient" | Cleverness is a readability tax. If it needs a comment to understand, simplify it. |
|
||||||
|
|
||||||
|
## Red Flags
|
||||||
|
|
||||||
|
- PRs merged without any review
|
||||||
|
- Review that only checks if tests pass (ignoring other axes)
|
||||||
|
- "LGTM" without evidence of actual review
|
||||||
|
- Security-sensitive changes without security-focused review
|
||||||
|
- Large PRs that are "too big to review properly" (split them)
|
||||||
|
- No regression tests with bug fix PRs
|
||||||
|
- Accepting "I'll fix it later" — it never happens
|
||||||
|
- A refactor that moves code around without reducing the number of concepts a reader must hold
|
||||||
|
- New conditionals scattered into unrelated code paths (a missing abstraction)
|
||||||
|
- A bespoke helper that duplicates an existing canonical one
|
||||||
|
- A bulk "bump dependencies" PR with no changelog review
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After review is complete:
|
||||||
|
|
||||||
|
- [ ] All Critical issues are resolved
|
||||||
|
- [ ] All Required (no-prefix) changes are resolved or explicitly deferred with justification
|
||||||
|
- [ ] Tests pass
|
||||||
|
- [ ] Build succeeds
|
||||||
|
- [ ] The verification story is documented (what changed, how it was verified)
|
||||||
|
- [ ] Dependency upgrades reviewed against changelog, isolated per package, verified by green suite
|
||||||
|
|
||||||
|
## Multi-Model Review Pattern
|
||||||
|
|
||||||
|
Use different models for different review perspectives:
|
||||||
|
|
||||||
|
```
|
||||||
|
Model A writes the code → Model B reviews → Model A addresses feedback → Human makes the final call
|
||||||
|
```
|
||||||
|
|
||||||
|
Different models have different blind spots.
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- For detailed security review guidance, see `security-and-hardening`
|
||||||
47
.opencode/skills/create-commit/SKILL.md
Normal file
47
.opencode/skills/create-commit/SKILL.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
name: create-commit
|
||||||
|
description: Stage, review, and commit files following Penpot commit conventions.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Skill: create-commit
|
||||||
|
|
||||||
|
Produce a git commit that follows Penpot's commit message conventions. This
|
||||||
|
skill owns the commit format, staging review, and safety checks — it does not
|
||||||
|
implement features or push.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- After code changes are complete and files need to be committed
|
||||||
|
- When delegated by a workflow step (e.g. implement-plan) to handle the commit
|
||||||
|
|
||||||
|
## Required Reading
|
||||||
|
|
||||||
|
Before drafting any commit, read `mem:workflow/creating-commits` end-to-end. It
|
||||||
|
is the authoritative source for the commit message format, the emoji menu,
|
||||||
|
subject/body limits, and the `AI-assisted-by` trailer. Follow it exactly.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Stage the files** specified by the calling context. Do not ask for
|
||||||
|
confirmation.
|
||||||
|
2. Run `git diff --staged` to review the content. If you see secrets (API keys,
|
||||||
|
tokens, passwords, private keys, `.env` values), debug prints, or anything
|
||||||
|
that does not match the stated intent, **STOP** and tell the user before
|
||||||
|
committing.
|
||||||
|
3. Draft the message following the format in the memory doc, wrapping the body
|
||||||
|
at 72 characters per line, and run:
|
||||||
|
```bash
|
||||||
|
git commit -m "<subject>" -m "<body>"
|
||||||
|
```
|
||||||
|
(or `git commit -F -` if the body has unusual characters).
|
||||||
|
4. The `AI-assisted-by` trailer value is provided by the calling context — use
|
||||||
|
it verbatim.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Do not push. Pushing is a separate workflow handled by the user.
|
||||||
|
- Do not run `git reset`, `git checkout`, `git restore`, `git clean`, or `rm`.
|
||||||
|
- Do not pass `--author`. Author identity comes from the local git config.
|
||||||
|
- Do not amend a commit you did not create in this session, unless explicitly asked.
|
||||||
|
- Do not bypass pre-commit hooks (`--no-verify`) unless explicitly asked.
|
||||||
|
- Do not add untracked files that were not created in this session.
|
||||||
@ -1,19 +1,19 @@
|
|||||||
---
|
---
|
||||||
name: nrepl-eval
|
name: nrepl-eval
|
||||||
description: Evaluate Clojure code via nREPL using the standalone tools/nrepl-eval.mjs CLI tool.
|
description: Evaluate Clojure code via nREPL using the standalone scripts/nrepl-eval.mjs CLI tool.
|
||||||
---
|
---
|
||||||
|
|
||||||
# nREPL Eval
|
# nREPL Eval
|
||||||
|
|
||||||
Evaluate Clojure (or ClojureScript) code via a running nREPL server using
|
Evaluate Clojure (or ClojureScript) code via a running nREPL server using
|
||||||
`tools/nrepl-eval.mjs`.
|
`scripts/nrepl-eval.mjs`.
|
||||||
|
|
||||||
Full documentation: `mem:tools/nrepl-eval` (file: `.serena/memories/tools/nrepl-eval.md`)
|
Full documentation: `mem:scripts/nrepl-eval` (file: `.serena/memories/scripts/nrepl-eval.md`)
|
||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs [options] [<code>]
|
./scripts/nrepl-eval.mjs [options] [<code>]
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
@ -30,8 +30,8 @@ Full documentation: `mem:tools/nrepl-eval` (file: `.serena/memories/tools/nrepl-
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs '(+ 1 2 3)'
|
./scripts/nrepl-eval.mjs '(+ 1 2 3)'
|
||||||
./tools/nrepl-eval.mjs --backend '(+ 1 2 3)'
|
./scripts/nrepl-eval.mjs --backend '(+ 1 2 3)'
|
||||||
./tools/nrepl-eval.mjs --frontend '(js/alert "hi")'
|
./scripts/nrepl-eval.mjs --frontend '(js/alert "hi")'
|
||||||
./tools/nrepl-eval.mjs -e
|
./scripts/nrepl-eval.mjs -e
|
||||||
```
|
```
|
||||||
|
|||||||
315
.opencode/skills/plan-review/SKILL.md
Normal file
315
.opencode/skills/plan-review/SKILL.md
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
---
|
||||||
|
name: plan-review
|
||||||
|
description: Reviews implementation plans for quality, completeness, and actionability. Use after a plan is produced by the planner skill, before starting implementation. Use when evaluating a plan written by yourself, another agent, or a human.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Plan Review
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Multi-dimensional plan review with quality gates. Every plan gets reviewed before implementation starts — no exceptions. Review covers six axes: completeness, task quality, architecture & sequencing, risk coverage, actionability, and proposed code quality.
|
||||||
|
|
||||||
|
**The approval standard:** Approve a plan when it is specific enough that a skilled implementer could execute it without guessing, the task ordering is sound, and risks are acknowledged. Perfect plans don't exist — the goal is confidence that implementation won't derail. Don't block a plan because it isn't exactly how you would have structured it. If it's executable and well-organized, approve it.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- After the planner skill produces a plan
|
||||||
|
- Before starting implementation on any non-trivial task
|
||||||
|
- When reviewing a plan written by another agent or a human
|
||||||
|
- When a plan feels too large, vague, or risky to start
|
||||||
|
|
||||||
|
**Do NOT use for:** Single-file changes with obvious scope, or when the task is trivial enough to just do.
|
||||||
|
|
||||||
|
## The Six-Axis Review
|
||||||
|
|
||||||
|
Every plan gets evaluated across these dimensions:
|
||||||
|
|
||||||
|
### 1. Completeness
|
||||||
|
|
||||||
|
Does the plan cover everything needed to implement successfully?
|
||||||
|
|
||||||
|
- Is the **context** clear? (What problem, why now, what's the goal?)
|
||||||
|
- Are **affected modules** identified with paths?
|
||||||
|
- Are **architecture decisions** documented with rationale?
|
||||||
|
- Is there a **testing strategy**?
|
||||||
|
- Are **verification commands** explicit (not "run the tests")?
|
||||||
|
- Are **open questions** listed (not buried in someone's head)?
|
||||||
|
- Is there a **parallelization** assessment for multi-task plans?
|
||||||
|
|
||||||
|
**Missing any of these is a gap, not a nit.**
|
||||||
|
|
||||||
|
### 2. Task Quality
|
||||||
|
|
||||||
|
Are the tasks well-defined and independently executable?
|
||||||
|
|
||||||
|
- Does every task have **acceptance criteria**? (Testable, not vague)
|
||||||
|
- Does every task have **verification steps**?
|
||||||
|
- Are tasks **sized appropriately**? (XS–M is ideal, L is acceptable, XL must be split)
|
||||||
|
- Are **dependencies** between tasks explicitly stated?
|
||||||
|
- Are **files likely touched** listed?
|
||||||
|
- Is each task a **single, self-contained change**? (Not "implement the whole feature")
|
||||||
|
- Could a skilled implementer pick up any task and execute it without asking clarifying questions?
|
||||||
|
|
||||||
|
### 3. Architecture & Sequencing
|
||||||
|
|
||||||
|
Is the plan structured so implementation flows correctly?
|
||||||
|
|
||||||
|
- Does implementation order follow the **dependency graph** (foundations first)?
|
||||||
|
- Are tasks **vertically sliced** (feature paths) rather than horizontally layered?
|
||||||
|
- Does each task leave the system in a **working state**?
|
||||||
|
- Are there **checkpoints** between major phases?
|
||||||
|
- Are **high-risk tasks early** (fail fast)?
|
||||||
|
- Is the total plan a reasonable number of tasks? (More than ~15 tasks suggests the scope should be split into multiple plans)
|
||||||
|
|
||||||
|
### 4. Risk Coverage
|
||||||
|
|
||||||
|
Are the hard parts acknowledged and mitigated?
|
||||||
|
|
||||||
|
- Are **edge cases** identified?
|
||||||
|
- Are **breaking changes** or **migration concerns** noted?
|
||||||
|
- Are **security implications** considered?
|
||||||
|
- Are **performance implications** considered?
|
||||||
|
- Are **external dependencies** or integration risks flagged?
|
||||||
|
- Is there a plan for **rollback** if something goes wrong?
|
||||||
|
- Are **data integrity** risks addressed (what happens if a migration fails mid-way)?
|
||||||
|
|
||||||
|
### 5. Actionability
|
||||||
|
|
||||||
|
Can an implementer actually execute this?
|
||||||
|
|
||||||
|
- Are **file paths** specific (not "update the relevant files")?
|
||||||
|
- Are **function/method names** mentioned where applicable?
|
||||||
|
- Are **verification commands** copy-pasteable (not "run the linter")?
|
||||||
|
- Are **test commands** project-specific (not generic)?
|
||||||
|
- Is the **code shape** described where the implementation isn't obvious?
|
||||||
|
- Are **conventions** referenced (naming, patterns, existing utilities to reuse)?
|
||||||
|
- Does the plan reference **existing code** the implementer should read first?
|
||||||
|
|
||||||
|
### 6. Proposed Code Quality *(when the plan includes implementation details)*
|
||||||
|
|
||||||
|
If the plan proposes code shapes, function signatures, data structures, or API designs, evaluate those proposals against `code-review` criteria:
|
||||||
|
|
||||||
|
- **Correctness:** Do the proposed types/signatures handle edge cases (null, empty, boundaries)?
|
||||||
|
- **Readability:** Are proposed names descriptive and consistent with project conventions?
|
||||||
|
- **Architecture:** Do proposed abstractions follow existing patterns? Are they justified (not over-engineered)?
|
||||||
|
- **Security:** Do proposed APIs validate input at boundaries? Any injection/XSS vectors in the design?
|
||||||
|
- **Performance:** Do proposed data structures avoid N+1 patterns? Any unbounded operations in the design?
|
||||||
|
|
||||||
|
**When to apply:** Only when the plan includes specific code snippets, type definitions, API contracts, or function signatures. Plans that only describe "what" without showing "how" skip this axis.
|
||||||
|
|
||||||
|
## Structural Remedies
|
||||||
|
|
||||||
|
When you flag a structural problem in a plan, propose the fix — not just the problem:
|
||||||
|
|
||||||
|
- **A task is too large (XL):** Split it into vertical slices. Each slice should be independently testable.
|
||||||
|
- **Missing acceptance criteria:** Draft 2–3 specific, testable conditions for the task.
|
||||||
|
- **Wrong sequencing:** Identify the dependency and propose the correct order.
|
||||||
|
- **No checkpoints:** Suggest where checkpoints should go (typically after every 2–3 tasks).
|
||||||
|
- **Vague verification:** Replace "run tests" with the actual project command.
|
||||||
|
- **Horizontal slicing:** Restructure into vertical feature paths.
|
||||||
|
- **Missing risk section:** Draft the risks you can identify from the plan content.
|
||||||
|
|
||||||
|
Prefer the remedy that makes the plan immediately actionable over one that just flags the gap.
|
||||||
|
|
||||||
|
## Plan Sizing
|
||||||
|
|
||||||
|
Plans should be scoped to a single deliverable:
|
||||||
|
|
||||||
|
```
|
||||||
|
1–5 tasks → Good. A focused feature or bug fix.
|
||||||
|
6–10 tasks → Acceptable for a moderate feature.
|
||||||
|
11–15 tasks → Large. Consider splitting into phases.
|
||||||
|
15+ tasks → Too large. Split into multiple plans.
|
||||||
|
```
|
||||||
|
|
||||||
|
**What counts as "one plan":** A self-contained set of changes that delivers a single coherent capability. If you can describe the goal in one sentence, it's one plan.
|
||||||
|
|
||||||
|
## Categorize Findings
|
||||||
|
|
||||||
|
Label every comment with its severity so the author knows what's required vs optional:
|
||||||
|
|
||||||
|
| Prefix | Meaning | Author Action |
|
||||||
|
|--------|---------|---------------|
|
||||||
|
| *(no prefix)* | Required change | Must address before implementation starts |
|
||||||
|
| **Critical:** | Blocks implementation | Missing security consideration, data integrity risk, fundamentally wrong approach |
|
||||||
|
| **Nit:** | Minor, optional | Author may ignore — wording, formatting |
|
||||||
|
| **Optional:** / **Consider:** | Suggestion | Worth considering but not required |
|
||||||
|
| **FYI** | Informational only | No action needed — context for future reference |
|
||||||
|
|
||||||
|
**Lead with what matters.** Order findings by leverage: missing risks and wrong sequencing first, then task quality gaps, then completeness, then nits. If you have one critical sequencing problem and ten nits, the sequencing problem *is* the review.
|
||||||
|
|
||||||
|
## Review Process
|
||||||
|
|
||||||
|
### Step 1: Understand the Goal
|
||||||
|
|
||||||
|
Before evaluating structure, understand intent:
|
||||||
|
|
||||||
|
```
|
||||||
|
- What is this plan trying to accomplish?
|
||||||
|
- What problem does it solve?
|
||||||
|
- What does "done" look like?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Check Completeness First
|
||||||
|
|
||||||
|
Scan for missing sections before diving into content:
|
||||||
|
|
||||||
|
```
|
||||||
|
- Context present?
|
||||||
|
- Affected modules listed?
|
||||||
|
- Architecture decisions documented?
|
||||||
|
- Risks acknowledged?
|
||||||
|
- Testing strategy defined?
|
||||||
|
- Verification commands explicit?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Review Task Quality
|
||||||
|
|
||||||
|
Walk through each task:
|
||||||
|
|
||||||
|
```
|
||||||
|
For each task:
|
||||||
|
1. Can I tell exactly what to build?
|
||||||
|
2. Are acceptance criteria specific and testable?
|
||||||
|
3. Is the size reasonable (not XL)?
|
||||||
|
4. Are dependencies clear?
|
||||||
|
5. Would I know which files to touch?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Validate Sequencing
|
||||||
|
|
||||||
|
Check the dependency graph:
|
||||||
|
|
||||||
|
```
|
||||||
|
- Are foundations built first?
|
||||||
|
- Does each task leave the system working?
|
||||||
|
- Are checkpoints placed correctly?
|
||||||
|
- Are high-risk items early?
|
||||||
|
- Is it vertically sliced?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Assess Actionability
|
||||||
|
|
||||||
|
Put yourself in the implementer's shoes:
|
||||||
|
|
||||||
|
```
|
||||||
|
- Could I pick up task 1 and start coding without asking any questions?
|
||||||
|
- Are the verification commands copy-pasteable?
|
||||||
|
- Are file paths and function names specific?
|
||||||
|
- Is existing code referenced where I'd need to read it?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Verify the Verification Story
|
||||||
|
|
||||||
|
Check that the plan can actually confirm it worked:
|
||||||
|
|
||||||
|
```
|
||||||
|
- What tests should pass after implementation?
|
||||||
|
- What build/compile commands are relevant?
|
||||||
|
- What manual checks are needed?
|
||||||
|
- How do we know the feature works end-to-end?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Evaluate Proposed Code Quality *(if applicable)*
|
||||||
|
|
||||||
|
If the plan includes code snippets, types, or API designs:
|
||||||
|
|
||||||
|
```
|
||||||
|
- Load code-review skill for criteria
|
||||||
|
- Check proposed signatures for edge cases
|
||||||
|
- Verify naming follows project conventions
|
||||||
|
- Confirm abstractions follow existing patterns
|
||||||
|
- Scan for security vectors in proposed APIs
|
||||||
|
- Check for performance issues in proposed data structures
|
||||||
|
```
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Review: [Plan title]
|
||||||
|
|
||||||
|
### Completeness
|
||||||
|
- [ ] Context explains the problem and goal
|
||||||
|
- [ ] Affected modules are listed with paths
|
||||||
|
- [ ] Architecture decisions have rationale
|
||||||
|
- [ ] Testing strategy is defined
|
||||||
|
- [ ] Verification commands are explicit and project-specific
|
||||||
|
- [ ] Open questions are listed
|
||||||
|
|
||||||
|
### Task Quality
|
||||||
|
- [ ] Every task has acceptance criteria
|
||||||
|
- [ ] Every task has verification steps
|
||||||
|
- [ ] Tasks are sized XS–M (L acceptable, XL must be split)
|
||||||
|
- [ ] Task dependencies are stated
|
||||||
|
- [ ] Files likely touched are listed
|
||||||
|
|
||||||
|
### Architecture & Sequencing
|
||||||
|
- [ ] Order follows dependency graph (foundations first)
|
||||||
|
- [ ] Vertically sliced (not horizontal layers)
|
||||||
|
- [ ] Each task leaves system working
|
||||||
|
- [ ] Checkpoints exist between phases
|
||||||
|
- [ ] High-risk tasks are early
|
||||||
|
|
||||||
|
### Risk Coverage
|
||||||
|
- [ ] Edge cases identified
|
||||||
|
- [ ] Breaking changes / migrations noted
|
||||||
|
- [ ] Security implications considered
|
||||||
|
- [ ] Performance implications considered
|
||||||
|
- [ ] Rollback strategy exists (if applicable)
|
||||||
|
|
||||||
|
### Actionability
|
||||||
|
- [ ] File paths are specific
|
||||||
|
- [ ] Verification commands are copy-pasteable
|
||||||
|
- [ ] Existing code to read is referenced
|
||||||
|
- [ ] Conventions and patterns are noted
|
||||||
|
|
||||||
|
### Proposed Code Quality *(if plan includes implementation details)*
|
||||||
|
- [ ] Proposed types/signatures handle edge cases
|
||||||
|
- [ ] Proposed names follow project conventions
|
||||||
|
- [ ] Proposed abstractions follow existing patterns
|
||||||
|
- [ ] No security vectors in proposed APIs
|
||||||
|
- [ ] No performance issues in proposed structures
|
||||||
|
|
||||||
|
### Verdict
|
||||||
|
- [ ] **Approve** — Ready to implement
|
||||||
|
- [ ] **Request changes** — Gaps must be addressed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Rationalizations
|
||||||
|
|
||||||
|
| Rationalization | Reality |
|
||||||
|
|---|---|
|
||||||
|
| "I'll figure out the details during implementation" | That's how you discover blocking dependencies mid-task. Surface them now. |
|
||||||
|
| "The tasks are obvious, no need for criteria" | Write them anyway. Explicit criteria surface hidden assumptions. |
|
||||||
|
| "It's just a small feature, it doesn't need a plan" | Small features have edge cases too. 3 tasks with criteria takes 5 minutes. |
|
||||||
|
| "The plan is good enough" | "Good enough" without acceptance criteria means the implementer defines "done" — and they might define it differently. |
|
||||||
|
| "I'll add verification steps later" | Later never comes. The plan is the contract — define verification now. |
|
||||||
|
| "Risks are minimal" | Every change has risks. If you can't name them, you haven't thought about them. |
|
||||||
|
| "The file paths are obvious" | They're obvious to the author. The implementer might not know the codebase. |
|
||||||
|
| "The code in the plan is fine, it'll get reviewed later" | Plan-level code review catches design problems before implementation — fixing them after coding is more expensive. |
|
||||||
|
|
||||||
|
## Red Flags
|
||||||
|
|
||||||
|
- No acceptance criteria on any task
|
||||||
|
- Tasks that say "implement the feature" without specifics
|
||||||
|
- No verification steps anywhere in the plan
|
||||||
|
- All tasks are XL-sized
|
||||||
|
- No checkpoints between phases
|
||||||
|
- Dependency order isn't considered (e.g., API handler before domain model)
|
||||||
|
- No testing strategy
|
||||||
|
- Verification commands are generic ("run tests") instead of project-specific
|
||||||
|
- Plan has 20+ tasks (scope too large for one plan)
|
||||||
|
- No risk section on a plan with migrations, breaking changes, or security implications
|
||||||
|
- Horizontal slicing (all domain, then all services, then all API)
|
||||||
|
- File paths are vague ("update the relevant files")
|
||||||
|
- Missing open questions section despite stated unknowns
|
||||||
|
- Proposed code ignores project conventions or existing patterns
|
||||||
|
- Proposed types use gratuitous `any`/`unknown`/optional without justification
|
||||||
|
- Proposed APIs don't validate input at boundaries
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- For producing plans, use the `planner` skill
|
||||||
|
- For reviewing implemented code, use `code-review` — also the criteria source for axis 6
|
||||||
|
- For security-specific concerns, see `security-and-hardening`
|
||||||
|
- For testing strategy guidance, see `testing`
|
||||||
@ -56,7 +56,10 @@ and only weave in Penpot context when it is clearly relevant.
|
|||||||
- Ask clarifying questions if the intent is unclear or if critical information
|
- Ask clarifying questions if the intent is unclear or if critical information
|
||||||
is missing (e.g. target model, expected output format, tone, constraints).
|
is missing (e.g. target model, expected output format, tone, constraints).
|
||||||
Keep questions concise and grouped. Prefer to ask 1–4 questions at once
|
Keep questions concise and grouped. Prefer to ask 1–4 questions at once
|
||||||
rather than one at a time.
|
rather than one at a time. **Use the `question` tool** to ask them so the
|
||||||
|
user gets a structured multi-choice UI; reserve a plain `## Clarifying
|
||||||
|
questions` markdown section for cases where the `question` tool is
|
||||||
|
unavailable or the question is genuinely open-ended.
|
||||||
- Rewrite the prompt using prompt-engineering best practices (see below).
|
- Rewrite the prompt using prompt-engineering best practices (see below).
|
||||||
- Preserve the user's original intent — do not change the underlying task.
|
- Preserve the user's original intent — do not change the underlying task.
|
||||||
- When the user provides Penpot project context, weave in the relevant
|
- When the user provides Penpot project context, weave in the relevant
|
||||||
@ -105,10 +108,27 @@ Deliver the result in the response as two clearly separated blocks:
|
|||||||
changes you made and why (3–7 bullets max). Skip the rationale if the
|
changes you made and why (3–7 bullets max). Skip the rationale if the
|
||||||
changes are trivial.
|
changes are trivial.
|
||||||
|
|
||||||
If you asked clarifying questions, list them in a separate **Clarifying
|
If you asked clarifying questions via the `question` tool, stop and wait for
|
||||||
questions** section above the refined prompt and stop — do not produce a
|
the answers before producing a refined prompt. If the `question` tool was not
|
||||||
refined prompt until the user answers. If the user explicitly told you to
|
available and you asked the questions in chat, list them in a separate
|
||||||
proceed without questions (e.g. "just rewrite it"), make reasonable
|
**Clarifying questions** section above the refined prompt and stop — do not
|
||||||
|
produce a refined prompt until the user answers. If the user explicitly told
|
||||||
|
you to proceed without questions (e.g. "just rewrite it"), make reasonable
|
||||||
assumptions and note them under **Assumptions made** in the rationale block.
|
assumptions and note them under **Assumptions made** in the rationale block.
|
||||||
|
|
||||||
No file persistence — the refined prompt lives entirely in the response.
|
## File Persistence
|
||||||
|
|
||||||
|
Always persist the refined prompt to disk so it can be re-used later, versioned
|
||||||
|
in git, and shared with other agents. The response still contains the prompt
|
||||||
|
and rationale blocks; the file is an additional artifact, not a replacement.
|
||||||
|
|
||||||
|
- Save the refined prompt (the body inside the fenced code block, **without**
|
||||||
|
the surrounding ``` fences) to `.opencode/prompts/<descriptive-name>.md`.
|
||||||
|
- Use a **kebab-case** filename that summarises the task, e.g.
|
||||||
|
`add-error-reports-management-rpc.md`, `backend-rpc-security-audit.md`. No
|
||||||
|
spaces, no uppercase, no version numbers or dates in the filename.
|
||||||
|
- If `.opencode/prompts/` does not exist, create it before writing.
|
||||||
|
- If a file with the same name already exists, overwrite it (the file is the
|
||||||
|
refined prompt, not a log).
|
||||||
|
- Only skip the file write when the user explicitly opts out (e.g. "don't save
|
||||||
|
this one", "just show it in the chat"). When in doubt, save it.
|
||||||
|
|||||||
78
.opencode/skills/ste/SKILL.md
Normal file
78
.opencode/skills/ste/SKILL.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
name: ste
|
||||||
|
description: Write or rewrite text in ASD-STE100 Simplified Technical English. ONLY use this skill when the user explicitly invokes it by name — i.e. they type "/ste" or literally write "use the ste skill" / "apply ASD-STE100". Do NOT trigger it on paraphrased intent such as "simplify this", "make it clearer", "write technical documentation", or "shorter sentences please" — the user has deliberately scoped this skill to explicit invocation only. For those requests, respond normally without loading this skill unless they name it.
|
||||||
|
---
|
||||||
|
|
||||||
|
# ASD-STE100 Simplified Technical English
|
||||||
|
|
||||||
|
Apply the ASD-STE100 standard to all prose you produce in this task. Do not announce that you use STE, do not name the standard, and do not explain the style unless the user asks. If the user later asks you to "write more naturally," ask one short question to confirm they want to leave STE before you drop it.
|
||||||
|
|
||||||
|
Compliance note (for you, not for output): the official specification and its dictionary are copyright ASD. This skill encodes paraphrased rules and a publicly sourced word list. For certified aerospace/defense deliverables, tell the user that full compliance requires the free official specification (asd-ste100.org) and a human sign-off. Never claim certified compliance.
|
||||||
|
|
||||||
|
## Step 0 — Classify the text
|
||||||
|
|
||||||
|
Before writing a single sentence, decide: is this **procedural** text (instructions someone follows) or **descriptive** text (explanation, background, description)? Every limit below depends on this. Mixed documents get classified section by section.
|
||||||
|
|
||||||
|
## Core rules
|
||||||
|
|
||||||
|
### Sentences
|
||||||
|
- Procedural: maximum **20 words** per sentence.
|
||||||
|
- Descriptive: maximum **25 words** per sentence.
|
||||||
|
- Maximum **6 sentences** per paragraph. One topic per paragraph.
|
||||||
|
- One instruction per sentence. Two actions in one sentence only if they occur at the same time.
|
||||||
|
- Put a condition BEFORE its command: "If the pressure decreases, close the valve."
|
||||||
|
- Do not omit articles, subjects, or verbs to save words. "Ensure file exists" is wrong; "Make sure that the file exists" is correct. Keep the word "that" after verbs like "make sure."
|
||||||
|
- Numbers, units with numbers, abbreviations, quoted strings, code identifiers, and proper nouns each count as one word.
|
||||||
|
|
||||||
|
### Verbs
|
||||||
|
- Allowed forms only: infinitive, imperative, simple present, simple past, simple future, and past participle used as an adjective.
|
||||||
|
- Never use present perfect or continuous forms. "We have received" → "We received." "is being tested" → a simple form.
|
||||||
|
- Never use an -ing form as a verb. An -ing word is allowed only inside a technical name ("the mounting bracket," "logging").
|
||||||
|
- Active voice. Passive is allowed only in descriptive text when the agent is unknown or unimportant.
|
||||||
|
- Instructions use the imperative: "Open the panel," not "You must open the panel" or "The panel should be opened."
|
||||||
|
- Express actions as verbs, not nouns: "compress the file," not "perform compression of the file."
|
||||||
|
- Modals: use **can** (possibility), **will** (future), **must** (requirement). Do not use should, would, could, may, might. A hedge becomes a fact or a "can": "an explosion can occur."
|
||||||
|
- No phrasal verbs: "go down" → "decrease," "set up" → "install," "carry out" → "do."
|
||||||
|
|
||||||
|
### Words
|
||||||
|
- One word, one meaning, one part of speech, used consistently. Never rotate synonyms: pick one name for a thing and repeat it.
|
||||||
|
- Before drafting, replace unapproved vocabulary. Read `references/word-substitutions.md` and apply it; it is the working dictionary for this skill.
|
||||||
|
- Domain-specific nouns (part names, tool names, product names, UI labels) and domain verbs (drill, ream, boot, compile) are your **technical nouns/verbs** — keep them as-is, use each consistently, and do not verb a noun or noun a verb.
|
||||||
|
- Noun clusters: maximum **3 words** ("overhead panel light" is the limit). Longer clusters get decomposed with prepositions or hyphenated on first use: "main-gear-door retraction-winch handle."
|
||||||
|
- American English spelling.
|
||||||
|
- No Latin abbreviations: "e.g." → "for example," "i.e." → "that is," delete "etc."
|
||||||
|
|
||||||
|
### Punctuation
|
||||||
|
- No semicolons — write two sentences.
|
||||||
|
- Parentheses only for references, abbreviations, and item numbers.
|
||||||
|
- Hyphenate words that act as one unit; a hyphenated word counts as one word.
|
||||||
|
- No contractions.
|
||||||
|
|
||||||
|
### Warnings, cautions, notes
|
||||||
|
- **WARNING** = risk of injury or death. **CAUTION** = risk of damage. **NOTE** = information only, never an instruction.
|
||||||
|
- Start a warning or caution with the command or condition, then give the risk:
|
||||||
|
"WARNING: Do not touch the terminal. The terminal has a dangerous voltage."
|
||||||
|
- Notes obey the 25-word descriptive limit.
|
||||||
|
|
||||||
|
## Step 2 — Self-check pass
|
||||||
|
|
||||||
|
After drafting, scan your text once for each of these and fix every hit before you respond:
|
||||||
|
|
||||||
|
1. Any sentence over the 20/25-word limit for its type
|
||||||
|
2. Contractions, semicolons
|
||||||
|
3. "should," "would," "could," "may," "might"
|
||||||
|
4. "has been," "have been," "had been," "is being," "was being"
|
||||||
|
5. -ing words used as verbs
|
||||||
|
6. Missing articles (a/an/the/this) before nouns
|
||||||
|
7. Synonym rotation (the same object under two names)
|
||||||
|
8. Any word in the unapproved column of `references/word-substitutions.md`
|
||||||
|
9. Warnings that state the risk before the command
|
||||||
|
|
||||||
|
## Reference files
|
||||||
|
|
||||||
|
- `references/word-substitutions.md` — unapproved → approved word mappings and one-meaning rulings. Read it before drafting; it is short.
|
||||||
|
- `references/examples.md` — worked before/after rewrites (procedural, descriptive, warnings, common mistakes). Read it when rewriting existing text or when unsure how a rule applies.
|
||||||
|
|
||||||
|
## What NOT to touch
|
||||||
|
|
||||||
|
Code blocks, command strings, file paths, error messages, quoted UI text, and proper nouns stay exactly as written. STE applies to the prose around them.
|
||||||
67
.opencode/skills/ste/references/examples.md
Normal file
67
.opencode/skills/ste/references/examples.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Worked before/after examples
|
||||||
|
|
||||||
|
## Verb forms
|
||||||
|
|
||||||
|
| Before | After |
|
||||||
|
|---|---|
|
||||||
|
| We have received the technical reports from HQ. | We received the technical reports from HQ. |
|
||||||
|
| This device has been being used at Boeing since 2005. | Boeing started to use this device in 2005. |
|
||||||
|
| The test is continued by the operator. | Continue the test. |
|
||||||
|
| The screws should be replaced. | Replace the screws. |
|
||||||
|
| The system is currently running diagnostics. | The system does diagnostic tests now. |
|
||||||
|
|
||||||
|
## Vocabulary and phrasing
|
||||||
|
|
||||||
|
| Before | After |
|
||||||
|
|---|---|
|
||||||
|
| Ensure file exists before running. | Make sure that the file exists before you run the command. |
|
||||||
|
| Rotate the cover until the jacks are accessible. | Turn the cover until you can get access to the jacks. |
|
||||||
|
| Extend the jack until the wheels are clear of the ground. | Extend the jack until the wheels do not touch the ground. |
|
||||||
|
| Make sure that these steps are followed. | Obey these steps. |
|
||||||
|
| Utilize approximately 3 liters of water. | Use about 3 liters of water. |
|
||||||
|
| Perform a compression of the log files prior to upload. | Compress the log files before you upload them. |
|
||||||
|
|
||||||
|
## Noun clusters
|
||||||
|
|
||||||
|
| Before | After |
|
||||||
|
|---|---|
|
||||||
|
| Main gear door retraction winch handle | Main-gear-door retraction-winch handle |
|
||||||
|
| Runway light connection resistance calibration | Calibration of the resistance of the runway light connection |
|
||||||
|
| Remove the engine transmission housing attachment bolts. | Remove the bolts that attach the transmission housing to the engine. |
|
||||||
|
|
||||||
|
## Procedural rewrite (condition first, one instruction per sentence)
|
||||||
|
|
||||||
|
Before:
|
||||||
|
> You should check that the reservoir has been filled with the correct fluid and, after attaching a clear tube to the bleed screw while ensuring its free end is submerged, the pedal can be pumped and held so that opening the screw releases trapped air.
|
||||||
|
|
||||||
|
After:
|
||||||
|
> 1. Fill the reservoir with the correct fluid.
|
||||||
|
> 2. Attach a clear tube to the bleed screw.
|
||||||
|
> 3. Put the free end of the tube in a container of fluid.
|
||||||
|
> 4. Push the pedal three times. Hold the pedal down.
|
||||||
|
> 5. Open the bleed screw one half turn. Air and fluid flow into the tube.
|
||||||
|
> 6. Close the bleed screw. Release the pedal.
|
||||||
|
> 7. If air continues to come out, do steps 4 thru 6 again.
|
||||||
|
|
||||||
|
## Warnings and cautions (command first, then risk)
|
||||||
|
|
||||||
|
Before:
|
||||||
|
> Note that serious data loss may potentially occur if the --force flag is used against production.
|
||||||
|
|
||||||
|
After:
|
||||||
|
> CAUTION: Do not use the --force flag on the production database. The flag deletes the rows that do not match the source.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
> Touching the terminal could result in electrocution.
|
||||||
|
|
||||||
|
After:
|
||||||
|
> WARNING: Do not touch the terminal. The terminal has a dangerous voltage.
|
||||||
|
|
||||||
|
## Common mistakes checklist
|
||||||
|
|
||||||
|
- Dropped articles: "Insert pin in bracket" → "Insert the pin in the bracket."
|
||||||
|
- Synonym rotation: check/verify/confirm for the same action → one term, everywhere.
|
||||||
|
- Hedges: "you may want to," "it is recommended that" → an imperative or "must."
|
||||||
|
- Instruction buried in a NOTE: notes never instruct. Move the instruction to a numbered step.
|
||||||
|
- Semicolon joining two clauses → two sentences.
|
||||||
|
- "There are three bolts on the panel" → "The panel has three bolts."
|
||||||
68
.opencode/skills/ste/references/word-substitutions.md
Normal file
68
.opencode/skills/ste/references/word-substitutions.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# Word substitutions and one-meaning rulings
|
||||||
|
|
||||||
|
Compiled from public secondary sources (STEMG/ASD public pages, TechScribe, Acrolinx, training materials). This is a working approximation, not the official ASD dictionary. When a word is not listed here and feels formal or Latin-derived, prefer the shortest common alternative.
|
||||||
|
|
||||||
|
## Unapproved → approved
|
||||||
|
|
||||||
|
| Do not use | Use instead |
|
||||||
|
|---|---|
|
||||||
|
| utilize, leverage, employ | use |
|
||||||
|
| commence, initiate, begin, originate | start |
|
||||||
|
| terminate, cease, conclude | stop, end |
|
||||||
|
| ensure, verify, confirm, validate, check | make sure (that), examine |
|
||||||
|
| perform, conduct, execute, carry out | do |
|
||||||
|
| facilitate, assist | help |
|
||||||
|
| obtain, acquire, procure | get |
|
||||||
|
| sufficient, adequate | enough |
|
||||||
|
| approximately | about |
|
||||||
|
| prior to | before |
|
||||||
|
| subsequent to, following (prep.) | after |
|
||||||
|
| adjacent to | near |
|
||||||
|
| accomplish | do |
|
||||||
|
| additional, supplementary | more |
|
||||||
|
| attempt | try |
|
||||||
|
| require, necessitate | need, must |
|
||||||
|
| mandatory | necessary |
|
||||||
|
| indicate, signify | show |
|
||||||
|
| observe (=watch) | look at, examine |
|
||||||
|
| rotate | turn |
|
||||||
|
| deactivate | turn off, set to off |
|
||||||
|
| activate, energize (unless technical verb) | turn on, start |
|
||||||
|
| toxic | poisonous |
|
||||||
|
| in order to | to |
|
||||||
|
| via, by means of | through, with |
|
||||||
|
| due to, owing to | because of |
|
||||||
|
| in the event of/that | if |
|
||||||
|
| accessible | (rewrite: "you can get access to") |
|
||||||
|
| remainder | rest |
|
||||||
|
| demonstrate | show |
|
||||||
|
| modify, alter | change |
|
||||||
|
| construct, fabricate, build | assemble, make |
|
||||||
|
| retain | keep |
|
||||||
|
| locate (=find) | find |
|
||||||
|
| depress (a button) | push, press |
|
||||||
|
| proceed | continue, go |
|
||||||
|
|
||||||
|
## One meaning, one part of speech (canonical rulings)
|
||||||
|
|
||||||
|
- **close** — verb only: to move to a position that stops flow, or to operate a circuit breaker. The adjective is unapproved → use **near** ("do not go near the propeller").
|
||||||
|
- **test** — noun only: "do a test," never "test the system."
|
||||||
|
- **check** — do not use as a verb for verification → "make sure that" or "examine."
|
||||||
|
- **follow** — means only "come after." For rules and steps use **obey**: "Obey the safety instructions."
|
||||||
|
- **fall** — means only "move down by gravity." For quantities use **decrease**. Never the season.
|
||||||
|
- **oil** — noun only. "Oil the bearing" → "Put oil on the bearing" / "Lubricate the bearing."
|
||||||
|
- **right** — direction only, never "correct."
|
||||||
|
- **clear** — "without blockage." "Wheels are clear of the ground" → "wheels do not touch the ground."
|
||||||
|
- **help** — verb only; the noun is **aid** ("with the aid of a mirror").
|
||||||
|
- **above / below** — physical position only. For quantities: **more than / less than**.
|
||||||
|
- **about** — two approved senses: "approximately" and "on the subject of." Use carefully.
|
||||||
|
- **turn** — the general verb for rotation; "turn on / turn off" for power state is standard.
|
||||||
|
- **level** — approved as noun and adjective (documented exception to the one-POS rule).
|
||||||
|
|
||||||
|
## Frequent-offender function words
|
||||||
|
|
||||||
|
- **should / would / could / may / might** — never. Requirement → **must**. Possibility → **can**. Future → **will**.
|
||||||
|
- **etc.** — delete, or write the full list.
|
||||||
|
- **e.g. / i.e.** — "for example" / "that is."
|
||||||
|
- **any / appropriate / applicable / relevant** as hedges — replace with the specific thing meant.
|
||||||
|
- **there is / there are** openers — rewrite with a real subject: "There are three bolts on the panel" → "The panel has three bolts."
|
||||||
@ -13,7 +13,7 @@ Fetch information from Taiga public API for the **Penpot** project
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- `python3` — the `tools/taiga.py` CLI script is self-contained (stdlib only)
|
- `python3` — the `scripts/taiga.py` CLI script is self-contained (stdlib only)
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@ -21,17 +21,17 @@ The easiest way is to use the bundled Python script:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Pass a Taiga URL directly
|
# Pass a Taiga URL directly
|
||||||
python3 tools/taiga.py https://tree.taiga.io/project/penpot/issue/13714
|
python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714
|
||||||
|
|
||||||
# Or use "<type> <ref>" syntax
|
# Or use "<type> <ref>" syntax
|
||||||
python3 tools/taiga.py us 14128
|
python3 scripts/taiga.py us 14128
|
||||||
python3 tools/taiga.py task 13648
|
python3 scripts/taiga.py task 13648
|
||||||
|
|
||||||
# Add --json for raw output
|
# Add --json for raw output
|
||||||
python3 tools/taiga.py --json issue 13714
|
python3 scripts/taiga.py --json issue 13714
|
||||||
|
|
||||||
# See full usage
|
# See full usage
|
||||||
python3 tools/taiga.py --help
|
python3 scripts/taiga.py --help
|
||||||
```
|
```
|
||||||
|
|
||||||
## URL Pattern Reference
|
## URL Pattern Reference
|
||||||
@ -51,30 +51,30 @@ To extract the **type** and **ref** from a URL:
|
|||||||
|
|
||||||
## Python Script Reference
|
## Python Script Reference
|
||||||
|
|
||||||
The `tools/taiga.py` script wraps the Taiga API into a single convenient CLI
|
The `scripts/taiga.py` script wraps the Taiga API into a single convenient CLI
|
||||||
with sensible defaults.
|
with sensible defaults.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
python3 tools/taiga.py <taiga-url>
|
python3 scripts/taiga.py <taiga-url>
|
||||||
python3 tools/taiga.py <type> <ref>
|
python3 scripts/taiga.py <type> <ref>
|
||||||
python3 tools/taiga.py [--json] <taiga-url>
|
python3 scripts/taiga.py [--json] <taiga-url>
|
||||||
python3 tools/taiga.py [--json] <type> <ref>
|
python3 scripts/taiga.py [--json] <type> <ref>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# By URL (recommended — no need to think about type/ref)
|
# By URL (recommended — no need to think about type/ref)
|
||||||
python3 tools/taiga.py https://tree.taiga.io/project/penpot/issue/13714
|
python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714
|
||||||
|
|
||||||
# By type and ref
|
# By type and ref
|
||||||
python3 tools/taiga.py us 14128
|
python3 scripts/taiga.py us 14128
|
||||||
python3 tools/taiga.py task 13648
|
python3 scripts/taiga.py task 13648
|
||||||
|
|
||||||
# Raw JSON output
|
# Raw JSON output
|
||||||
python3 tools/taiga.py --json issue 13714
|
python3 scripts/taiga.py --json issue 13714
|
||||||
```
|
```
|
||||||
|
|
||||||
### Output
|
### Output
|
||||||
|
|||||||
57
.opencode/skills/testing/SKILL.md
Normal file
57
.opencode/skills/testing/SKILL.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
name: testing
|
||||||
|
description: Enforce TDD workflow and testing best practices for Penpot. Use when implementing features, fixing bugs, or modifying behavior. Reads testing memory for full guidance.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Testing Skill
|
||||||
|
|
||||||
|
Enforces test-driven development and Penpot testing conventions.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Implementing new logic or behavior
|
||||||
|
- Fixing any bug (reproduction test required)
|
||||||
|
- Modifying existing functionality
|
||||||
|
- Adding edge case handling
|
||||||
|
|
||||||
|
**Skip:** Pure configuration changes, documentation updates, or static content with no behavioral impact.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
Follow TDD (Red → Green → Refactor) whenever practical:
|
||||||
|
|
||||||
|
1. **RED** — Write a failing test first
|
||||||
|
2. **GREEN** — Write minimal code to pass
|
||||||
|
3. **REFACTOR** — Clean up while tests stay green
|
||||||
|
|
||||||
|
For bug fixes, use the Prove-It Pattern: write a test that reproduces the bug, confirm it fails, implement the fix, confirm it passes.
|
||||||
|
|
||||||
|
## Required Reading
|
||||||
|
|
||||||
|
Before writing any test, read:
|
||||||
|
|
||||||
|
1. `.serena/memories/testing.md` — cross-cutting testing principles, TDD workflow, anti-patterns, execution discipline
|
||||||
|
2. Module-specific testing memory for the affected module:
|
||||||
|
- `mem:common/testing` — CLJC unit tests
|
||||||
|
- `mem:frontend/testing` — CLJS unit tests, Playwright E2E
|
||||||
|
- `mem:backend/core` — JVM clojure.test conventions
|
||||||
|
|
||||||
|
## Key Rules
|
||||||
|
|
||||||
|
- Every behavior change needs a test
|
||||||
|
- Test state, not interactions
|
||||||
|
- DAMP over DRY — tests are specifications; duplication is OK if each test is self-contained and readable
|
||||||
|
- Prefer Real > Fake > Stub > Mock
|
||||||
|
- Arrange-Act-Assert structure
|
||||||
|
- One assertion per concept
|
||||||
|
- Never pipe test output to filters — redirect to file first
|
||||||
|
- Register new test files in the module's runner/entrypoint
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After completing implementation:
|
||||||
|
|
||||||
|
- [ ] Every new behavior has a test
|
||||||
|
- [ ] All tests pass for touched modules
|
||||||
|
- [ ] Bug fixes include a reproduction test
|
||||||
|
- [ ] Lint/formatter passes
|
||||||
@ -20,7 +20,7 @@ primary link, with the fix PR inline on the same line.
|
|||||||
|
|
||||||
- `gh` CLI authenticated (`gh auth status`)
|
- `gh` CLI authenticated (`gh auth status`)
|
||||||
- Python 3.8+
|
- Python 3.8+
|
||||||
- `tools/gh.py` helper script available
|
- `scripts/gh.py` helper script available
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
@ -36,13 +36,13 @@ Use the helper script. It uses GraphQL for efficient single-pass fetching
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# All closed issues (default)
|
# All closed issues (default)
|
||||||
python3 tools/gh.py issues "2.16.0"
|
python3 scripts/gh.py issues "2.16.0"
|
||||||
|
|
||||||
# Include open issues too
|
# Include open issues too
|
||||||
python3 tools/gh.py issues "2.16.0" --state all
|
python3 scripts/gh.py issues "2.16.0" --state all
|
||||||
|
|
||||||
# Exclude entries that should not go in the changelog
|
# Exclude entries that should not go in the changelog
|
||||||
python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog"
|
python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Exclusion rules (issue-level):**
|
**Exclusion rules (issue-level):**
|
||||||
@ -68,7 +68,7 @@ If updating from an existing `CHANGES.md`, find issues in the milestone that
|
|||||||
are NOT yet referenced in the changelog:
|
are NOT yet referenced in the changelog:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog" --compare CHANGES.md
|
python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog" --compare CHANGES.md
|
||||||
```
|
```
|
||||||
|
|
||||||
This returns a filtered JSON array with only the missing issues.
|
This returns a filtered JSON array with only the missing issues.
|
||||||
@ -85,23 +85,23 @@ community contribution attribution, or to read the PR body for
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# One or more PR numbers
|
# One or more PR numbers
|
||||||
python3 tools/gh.py prs 9179 9204 9311
|
python3 scripts/gh.py prs 9179 9204 9311
|
||||||
|
|
||||||
# From a file
|
# From a file
|
||||||
python3 tools/gh.py prs --file prs.txt
|
python3 scripts/gh.py prs --file prs.txt
|
||||||
|
|
||||||
# From stdin
|
# From stdin
|
||||||
cat prs.txt | python3 tools/gh.py prs --stdin
|
cat prs.txt | python3 scripts/gh.py prs --stdin
|
||||||
```
|
```
|
||||||
|
|
||||||
The `prs` command also supports listing all PRs in a milestone in one call:
|
The `prs` command also supports listing all PRs in a milestone in one call:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# All merged PRs in a milestone (default)
|
# All merged PRs in a milestone (default)
|
||||||
python3 tools/gh.py prs --milestone "2.16.0"
|
python3 scripts/gh.py prs --milestone "2.16.0"
|
||||||
|
|
||||||
# All states (merged, open, closed)
|
# All states (merged, open, closed)
|
||||||
python3 tools/gh.py prs --milestone "2.16.0" --state all
|
python3 scripts/gh.py prs --milestone "2.16.0" --state all
|
||||||
```
|
```
|
||||||
|
|
||||||
The `prs` command returns JSON with `number`, `title`, `body`, `state`,
|
The `prs` command returns JSON with `number`, `title`, `body`, `state`,
|
||||||
@ -113,13 +113,13 @@ You can also list all PRs in a milestone in a single call:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# All merged PRs in a milestone (default)
|
# All merged PRs in a milestone (default)
|
||||||
python3 tools/gh.py prs --milestone "2.16.0"
|
python3 scripts/gh.py prs --milestone "2.16.0"
|
||||||
|
|
||||||
# All states (merged, open, closed)
|
# All states (merged, open, closed)
|
||||||
python3 tools/gh.py prs --milestone "2.16.0" --state all
|
python3 scripts/gh.py prs --milestone "2.16.0" --state all
|
||||||
|
|
||||||
# Open PRs only
|
# Open PRs only
|
||||||
python3 tools/gh.py prs --milestone "2.16.0" --state open
|
python3 scripts/gh.py prs --milestone "2.16.0" --state open
|
||||||
```
|
```
|
||||||
|
|
||||||
The milestone path uses paginated GraphQL on the milestone's `pullRequests`
|
The milestone path uses paginated GraphQL on the milestone's `pullRequests`
|
||||||
@ -147,6 +147,12 @@ belongs to.
|
|||||||
The `gh.py` issues command already includes `issue_type` in every entry's
|
The `gh.py` issues command already includes `issue_type` in every entry's
|
||||||
output. **No separate GraphQL query is needed.**
|
output. **No separate GraphQL query is needed.**
|
||||||
|
|
||||||
|
**Preserve highlighted entries:** If an entry is already featured in
|
||||||
|
`### :rocket: Epics and highlights`, keep it in that section when refreshing a
|
||||||
|
changelog version. Do not remove a highlighted entry just because issue type
|
||||||
|
categorization would otherwise place it under `### :sparkles: New features &
|
||||||
|
Enhancements`.
|
||||||
|
|
||||||
**Community contribution attribution:** If the issue or its fix PR has the
|
**Community contribution attribution:** If the issue or its fix PR has the
|
||||||
`community contribution` label, add an attribution `(by @<github_username>)`
|
`community contribution` label, add an attribution `(by @<github_username>)`
|
||||||
on the changelog entry line, **before** the GitHub issue/PR references.
|
on the changelog entry line, **before** the GitHub issue/PR references.
|
||||||
@ -155,7 +161,7 @@ The attribution should reference the **PR author**, not the issue author.
|
|||||||
The `prs` subcommand includes the `author` field — use that:
|
The `prs` subcommand includes the `author` field — use that:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 tools/gh.py prs <PR_NUMBER> | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['author'])"
|
python3 scripts/gh.py prs <PR_NUMBER> | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['author'])"
|
||||||
```
|
```
|
||||||
|
|
||||||
Placement in the entry line:
|
Placement in the entry line:
|
||||||
@ -190,7 +196,7 @@ only reference **merged** PRs. Verify before writing:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Collect all PR numbers from the candidate entries and check them
|
# Collect all PR numbers from the candidate entries and check them
|
||||||
python3 tools/gh.py prs <ALL_PR_NUMBERS> | python3 -c "
|
python3 scripts/gh.py prs <ALL_PR_NUMBERS> | python3 -c "
|
||||||
import json, sys
|
import json, sys
|
||||||
for pr in json.load(sys.stdin):
|
for pr in json.load(sys.stdin):
|
||||||
if pr['state'] != 'MERGED':
|
if pr['state'] != 'MERGED':
|
||||||
@ -265,7 +271,7 @@ section) or in the candidate set for the current milestone, check:
|
|||||||
current milestone since the changelog was last updated (e.g., a fix
|
current milestone since the changelog was last updated (e.g., a fix
|
||||||
arrived late and the issue was reassigned to a future milestone)?
|
arrived late and the issue was reassigned to a future milestone)?
|
||||||
- Verify the issue is still in the current milestone via
|
- Verify the issue is still in the current milestone via
|
||||||
`python3 tools/gh.py issues <MILESTONE> --state all`. If it's no
|
`python3 scripts/gh.py issues <MILESTONE> --state all`. If it's no
|
||||||
longer there, remove the entry from the current section. (If the
|
longer there, remove the entry from the current section. (If the
|
||||||
target section doesn't exist yet, the entry is simply dropped.)
|
target section doesn't exist yet, the entry is simply dropped.)
|
||||||
|
|
||||||
@ -337,7 +343,7 @@ cross-reference to catch gaps:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List all merged PRs in the milestone
|
# List all merged PRs in the milestone
|
||||||
python3 tools/gh.py prs --milestone "<MILESTONE>" --state merged > /tmp/milestone-prs.json
|
python3 scripts/gh.py prs --milestone "<MILESTONE>" --state merged > /tmp/milestone-prs.json
|
||||||
|
|
||||||
# Extract PR numbers from the changelog section
|
# Extract PR numbers from the changelog section
|
||||||
python3 -c "
|
python3 -c "
|
||||||
@ -378,7 +384,7 @@ changelog or is legitimately excluded (check its labels).
|
|||||||
Also verify that no closed-unmerged PRs remain in the changelog:
|
Also verify that no closed-unmerged PRs remain in the changelog:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 tools/gh.py prs --milestone "<MILESTONE>" --state all | python3 -c "
|
python3 scripts/gh.py prs --milestone "<MILESTONE>" --state all | python3 -c "
|
||||||
import json, sys
|
import json, sys
|
||||||
data = json.load(sys.stdin)
|
data = json.load(sys.stdin)
|
||||||
closed = [p for p in data if p['state'] == 'CLOSED']
|
closed = [p for p in data if p['state'] == 'CLOSED']
|
||||||
@ -483,13 +489,13 @@ def fmt_issue_list(nums):
|
|||||||
|
|
||||||
# --- Fetch milestone data ---
|
# --- Fetch milestone data ---
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["python3", "tools/gh.py", "issues", MILESTONE, "--state", "all"],
|
["python3", "scripts/gh.py", "issues", MILESTONE, "--state", "all"],
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True)
|
||||||
all_issues = json.loads(result.stdout)
|
all_issues = json.loads(result.stdout)
|
||||||
issue_by_num = {i['number']: i for i in all_issues}
|
issue_by_num = {i['number']: i for i in all_issues}
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["python3", "tools/gh.py", "prs", "--milestone", MILESTONE, "--state", "all"],
|
["python3", "scripts/gh.py", "prs", "--milestone", MILESTONE, "--state", "all"],
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True)
|
||||||
all_prs = json.loads(result.stdout)
|
all_prs = json.loads(result.stdout)
|
||||||
pr_by_num = {p['number']: p for p in all_prs}
|
pr_by_num = {p['number']: p for p in all_prs}
|
||||||
@ -728,7 +734,7 @@ self-contained and clickable in any Markdown viewer.
|
|||||||
reference if applicable.
|
reference if applicable.
|
||||||
- **Re-fetch before editing.** Milestones can change — always re-fetch issues
|
- **Re-fetch before editing.** Milestones can change — always re-fetch issues
|
||||||
before making edits, don't rely on cached data.
|
before making edits, don't rely on cached data.
|
||||||
- **Use `tools/gh.py`.** Prefer the helper script over raw `gh api` calls for
|
- **Use `scripts/gh.py`.** Prefer the helper script over raw `gh api` calls for
|
||||||
milestone issue listing and PR detail fetching. It handles GraphQL
|
milestone issue listing and PR detail fetching. It handles GraphQL
|
||||||
pagination, batching, and label filtering automatically.
|
pagination, batching, and label filtering automatically.
|
||||||
- **Verify PR merge status.** Not all closing PRs are merged — community PRs
|
- **Verify PR merge status.** Not all closing PRs are merged — community PRs
|
||||||
@ -739,7 +745,7 @@ self-contained and clickable in any Markdown viewer.
|
|||||||
labels. Check both.
|
labels. Check both.
|
||||||
- **Cross-reference milestone PRs, not just issues.** The `--compare` flag on
|
- **Cross-reference milestone PRs, not just issues.** The `--compare` flag on
|
||||||
the `issues` command only compares issue numbers. Merged PRs not linked to
|
the `issues` command only compares issue numbers. Merged PRs not linked to
|
||||||
any milestone issue can be missed. Use `python3 tools/gh.py prs --milestone`
|
any milestone issue can be missed. Use `python3 scripts/gh.py prs --milestone`
|
||||||
for a full PR cross-reference.
|
for a full PR cross-reference.
|
||||||
- **False-positive PR-to-issue associations.** A PR may claim to close an
|
- **False-positive PR-to-issue associations.** A PR may claim to close an
|
||||||
issue from a different project or context. If the PR title and issue title
|
issue from a different project or context. If the PR title and issue title
|
||||||
|
|||||||
@ -5,7 +5,8 @@ Backend: JVM Clojure; Integrant; PostgreSQL; Redis/Valkey; RPC; HTTP; storage; m
|
|||||||
## Focused memories
|
## Focused memories
|
||||||
|
|
||||||
- RPC, DB helpers, workers, cron: `mem:backend/rpc-db-worker-subtleties`
|
- RPC, DB helpers, workers, cron: `mem:backend/rpc-db-worker-subtleties`
|
||||||
- HTTP sessions, config, storage, media, file data persistence: `mem:backend/http-storage-filedata-subtleties`
|
- Storage abstraction, logical buckets, object lifecycle, deduplication, access, and garbage collection: `mem:backend/storage`.
|
||||||
|
- HTTP sessions, config, media processing, and file data persistence: `mem:backend/http-storage-filedata-subtleties`.
|
||||||
- Auth flows, permission model, teams, projects, invitations, comments, webhooks, audit: `mem:backend/auth-permissions-product-domains`
|
- Auth flows, permission model, teams, projects, invitations, comments, webhooks, audit: `mem:backend/auth-permissions-product-domains`
|
||||||
- Services, task-queue/Pub-Sub topology constraints -> `mem:prod-infra/core`.
|
- Services, task-queue/Pub-Sub topology constraints -> `mem:prod-infra/core`.
|
||||||
|
|
||||||
@ -39,8 +40,8 @@ Backend RPC command areas without focused memories include access tokens, binfil
|
|||||||
|
|
||||||
Database migrations live in `backend/src/app/migrations/`; pure SQL migrations are under `backend/src/app/migrations/sql/`. SQL filenames conventionally start with a sequence and verb/table description, e.g. `0026-mod-profile-table-add-is-active-field`. Applied migrations are tracked in the `migrations` table.
|
Database migrations live in `backend/src/app/migrations/`; pure SQL migrations are under `backend/src/app/migrations/sql/`. SQL filenames conventionally start with a sequence and verb/table description, e.g. `0026-mod-profile-table-add-is-active-field`. Applied migrations are tracked in the `migrations` table.
|
||||||
|
|
||||||
For interactive PostgreSQL access with correct dev defaults, use `tools/psql`; to dump
|
For interactive PostgreSQL access with correct dev defaults, use `scripts/psql`; to dump
|
||||||
the current DDL schema, use `tools/db-schema` (see `mem:tools/psql`).
|
the current DDL schema, use `scripts/db-schema` (see `mem:scripts/psql`).
|
||||||
|
|
||||||
For deeper details on transaction semantics, advisory locks, Transit vs JSON helpers, and dev/test DB URLs: `mem:backend/rpc-db-worker-subtleties`.
|
For deeper details on transaction semantics, advisory locks, Transit vs JSON helpers, and dev/test DB URLs: `mem:backend/rpc-db-worker-subtleties`.
|
||||||
|
|
||||||
@ -56,14 +57,14 @@ In devenv, backend nREPL is exposed on port 6064.
|
|||||||
|
|
||||||
### Non-interactive eval (preferred for agents)
|
### Non-interactive eval (preferred for agents)
|
||||||
|
|
||||||
`./tools/nrepl-eval.mjs` connects to an already-running nREPL server and evaluates code. Session state (defs, `in-ns`) persists across invocations via a stored session ID in `/tmp/penpot-nrepl-session-<host>-<port>`.
|
`./scripts/nrepl-eval.mjs` connects to an already-running nREPL server and evaluates code. Session state (defs, `in-ns`) persists across invocations via a stored session ID in `/tmp/penpot-nrepl-session-<host>-<port>`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs '(+ 1 2)' # single expression
|
./scripts/nrepl-eval.mjs '(+ 1 2)' # single expression
|
||||||
./tools/nrepl-eval.mjs "(require '[my.ns :as ns] :reload)" # reload after edits
|
./scripts/nrepl-eval.mjs "(require '[my.ns :as ns] :reload)" # reload after edits
|
||||||
./tools/nrepl-eval.mjs -e # inspect last exception (*e)
|
./scripts/nrepl-eval.mjs -e # inspect last exception (*e)
|
||||||
./tools/nrepl-eval.mjs --reset-session '(def x 0)' # discard session, start fresh
|
./scripts/nrepl-eval.mjs --reset-session '(def x 0)' # discard session, start fresh
|
||||||
./tools/nrepl-eval.mjs <<'EOF' # multi-expression heredoc
|
./scripts/nrepl-eval.mjs <<'EOF' # multi-expression heredoc
|
||||||
(def x 10)
|
(def x 10)
|
||||||
(+ x 20)
|
(+ x 20)
|
||||||
EOF
|
EOF
|
||||||
@ -92,12 +93,12 @@ Fixtures can populate local data for manual testing/perf work. From the backend
|
|||||||
|
|
||||||
IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory.
|
IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory.
|
||||||
|
|
||||||
* **Linting:** `pnpm run lint` from the repository root.
|
* **Linting:** `pnpm run lint:clj`.
|
||||||
* **Formatting:** `pnpm run check-fmt`. Use `pnpm run fmt` to fix. Avoid unrelated whitespace diffs.
|
* **Formatting:** `pnpm run check-fmt:clj` to check, `pnpm run fmt:clj` to fix. After running `fmt:clj`, `check-fmt:clj` is redundant. Avoid unrelated whitespace diffs.
|
||||||
|
|
||||||
**Before linting:** if delimiter errors are suspected (after LLM edits), run
|
**Before linting:** if delimiter errors are suspected (after LLM edits), run
|
||||||
`tools/paren-repair.bb` on the affected files first. Delimiter errors produce
|
`scripts/paren-repair` on the affected files first. Delimiter errors produce
|
||||||
misleading linter/compiler output. See `mem:tools/paren-repair`.
|
misleading linter/compiler output. See `mem:scripts/paren-repair`.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
@ -107,4 +108,3 @@ IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory. J
|
|||||||
* **Isolated run:** `clojure -M:dev:test --focus backend-tests.my-ns-test` for a specific test namespace.
|
* **Isolated run:** `clojure -M:dev:test --focus backend-tests.my-ns-test` for a specific test namespace.
|
||||||
* **Regression run:** `clojure -M:dev:test` to ensure no regressions in related functional areas.
|
* **Regression run:** `clojure -M:dev:test` to ensure no regressions in related functional areas.
|
||||||
* **Principles:** Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`.
|
* **Principles:** Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`.
|
||||||
|
|
||||||
|
|||||||
@ -14,10 +14,7 @@
|
|||||||
|
|
||||||
## Storage and media
|
## Storage and media
|
||||||
|
|
||||||
- Storage has a fixed valid bucket set. Backends are `:fs` and `:s3`; default backend comes from deprecated `assets-storage-backend` only when present, otherwise `objects-storage-backend`, defaulting to `:fs`.
|
- Storage abstraction, backend configuration, logical buckets, object lifecycle, deduplication, access rules, and garbage collection: `mem:backend/storage`.
|
||||||
- `put-object!` creates the DB `storage_object` row before writing backend content. Backend writes happen only for newly created rows, so deduplication can skip object writes.
|
|
||||||
- Deduplication only applies when requested, when the content can provide a hash, and when bucket metadata is present. Reads exclude soft-deleted storage rows.
|
|
||||||
- `sto/resolve` can reuse the current DB connection via `::db/reuse-conn true`; preserve this in transaction-sensitive code.
|
|
||||||
- SVG validation strips DOCTYPE and uses secure SAX parsing. Basic SVG info falls back to 100x100 dimensions when width/height/viewBox are missing.
|
- SVG validation strips DOCTYPE and uses secure SAX parsing. Basic SVG info falls back to 100x100 dimensions when width/height/viewBox are missing.
|
||||||
- Raster metadata is shell-derived with ImageMagick `identify`, verifies detected MIME against the supplied MIME, and swaps dimensions for EXIF orientations 6/8.
|
- Raster metadata is shell-derived with ImageMagick `identify`, verifies detected MIME against the supplied MIME, and swaps dimensions for EXIF orientations 6/8.
|
||||||
- Remote image download requires 2xx status, `content-length`, a known MIME, and size under the configured maximum before writing the temp file; mismatched byte count is an internal error.
|
- Remote image download requires 2xx status, `content-length`, a known MIME, and size under the configured maximum before writing the temp file; mismatched byte count is an internal error.
|
||||||
|
|||||||
83
.serena/memories/backend/storage.md
Normal file
83
.serena/memories/backend/storage.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# Backend Storage
|
||||||
|
|
||||||
|
## Abstraction
|
||||||
|
|
||||||
|
- `app.storage` stores binary objects.
|
||||||
|
- Each object has a `storage_object` database row.
|
||||||
|
- The row stores the UUID, size, backend, timestamps, and Transit metadata.
|
||||||
|
- The backend stores the binary content.
|
||||||
|
- Supported backends are `:fs` and `:s3`.
|
||||||
|
- FS uses one root directory and a UUID-derived path.
|
||||||
|
- S3 uses one configured bucket and an optional prefix.
|
||||||
|
- A Penpot bucket is metadata. It is not an S3 bucket or a filesystem directory.
|
||||||
|
- FS and S3 use the same UUID-derived object path. The bucket does not change the path.
|
||||||
|
- `PENPOT_OBJECTS_STORAGE_*` configures the current object backend.
|
||||||
|
- Deprecated asset-storage config keys remain supported for migration.
|
||||||
|
- Database rows keep the backend name. Keep the legacy `:assets-fs` and `:assets-s3` aliases.
|
||||||
|
|
||||||
|
## Object Lifecycle
|
||||||
|
|
||||||
|
- `put-object!` creates the database row before it writes backend content.
|
||||||
|
- Backend content is written only when the row is new.
|
||||||
|
- A failed backend write can leave an unreferenced database row.
|
||||||
|
- Callers often set `:touched-at` so garbage collection can remove such rows.
|
||||||
|
- `get-object` excludes rows with `deleted_at`.
|
||||||
|
- Existing object values can remain readable until physical deletion.
|
||||||
|
- `:expired-at` blocks reads after the expiration time.
|
||||||
|
- `del-object!` sets `deleted_at`. It does not remove backend content.
|
||||||
|
- `storage-gc-deleted` removes the database row and backend content after the deletion delay.
|
||||||
|
- `storage-gc-touched` finds references before it sets `deleted_at`.
|
||||||
|
- `objects-gc` removes deleted domain rows and touches their storage object IDs.
|
||||||
|
- Use `::db/reuse-conn true` with `sto/resolve` inside a database transaction.
|
||||||
|
|
||||||
|
## Deduplication
|
||||||
|
|
||||||
|
- Deduplication requires `::sto/deduplicate?`, a content hash, and bucket metadata.
|
||||||
|
- The lookup matches hash, bucket, backend, and `deleted_at IS NULL`.
|
||||||
|
- The lookup does not include file ID, profile ID, team ID, or organization ID.
|
||||||
|
- Objects can therefore share content across users and files within one bucket.
|
||||||
|
- Deleted objects are not reused.
|
||||||
|
- `tempfile` objects never use deduplication, even when the caller requests it.
|
||||||
|
- Use `sto/wrap-with-hash` when the caller already calculated the content hash.
|
||||||
|
|
||||||
|
## Bucket Rules
|
||||||
|
|
||||||
|
| Bucket | Content and references | Dedup | Direct `/assets/by-id` access | Cleanup |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `file-media-object` | Original file images and generated media thumbnails. References: `file_media_object.media_id` and `thumbnail_id`. | Yes | Public | Reference scan. |
|
||||||
|
| `team-font-variant` | Font variants in `team_font_variant`. References: `woff1_file_id`, `woff2_file_id`, `otf_file_id`, and `ttf_file_id`. | Yes | Public | Reference scan. |
|
||||||
|
| `file-object-thumbnail` | Frame and component thumbnails in `file_tagged_object_thumbnail.media_id`. | Yes | Public | Reference scan. |
|
||||||
|
| `file-thumbnail` | File grid thumbnails in `file_thumbnail.media_id`. | Yes | Authentication required | Reference scan. |
|
||||||
|
| `profile` | User and team profile photos. References: `profile.photo_id` and `team.photo_id`. | Yes | Authentication required | Reference scan. |
|
||||||
|
| `organization` | Organization logos uploaded by the Nitrate management API. | Yes | Public | No reference scan. A touched object is deleted. |
|
||||||
|
| `tempfile` | Export files, chunked-upload chunks, and temporary font downloads. | No | Authentication required | No reference scan. A touched object uses a two-hour deletion delay. |
|
||||||
|
| `file-data` | Encoded file data when `file-data-backend` is `storage`. Reference metadata has `storage-ref-id`, `file-id`, and the `file_data` row ID. | Yes | Authentication required | Reference scan. |
|
||||||
|
| `file-data-fragment` | Compatibility value for file-data fragments. The current backend has no dedicated producer for this bucket. | No current write semantics | Public | No touched-object collector case. |
|
||||||
|
| `file-change` | Compatibility value for file changes. Current snapshots store data in `file_data`, not this bucket. | No current write semantics | Authentication required | No touched-object collector case. |
|
||||||
|
|
||||||
|
- The valid bucket set lives in `app.storage/valid-buckets`.
|
||||||
|
- `file-media-object` is the default bucket for old rows without bucket metadata.
|
||||||
|
- Do not assign a new bucket without adding its access and cleanup behavior.
|
||||||
|
- The touched-object collector raises an internal error for an unknown bucket.
|
||||||
|
- It supports `file-media-object`, `team-font-variant`, `file-object-thumbnail`, `file-thumbnail`, `profile`, `file-data`, `tempfile`, and `organization`.
|
||||||
|
- It does not support `file-data-fragment` or `file-change`.
|
||||||
|
|
||||||
|
## Access Rules
|
||||||
|
|
||||||
|
- `app.http.assets` decides direct object authentication from the bucket.
|
||||||
|
- Public buckets are `file-media-object`, `file-object-thumbnail`, `team-font-variant`, `file-data-fragment`, and `organization`.
|
||||||
|
- Other valid buckets require a session or access-token profile ID.
|
||||||
|
- File-media routes also require file read permission.
|
||||||
|
- Non-public direct responses set `content-disposition: attachment`.
|
||||||
|
- FS responses use `x-accel-redirect` for the configured asset path.
|
||||||
|
- S3 responses use a presigned URL and an HTTP redirect.
|
||||||
|
|
||||||
|
## File Data
|
||||||
|
|
||||||
|
- `file-data-backend` accepts `legacy-db`, `db`, or `storage`.
|
||||||
|
- `legacy-db` stores main data in `file.data` and snapshots in `file_change.data`.
|
||||||
|
- `db` stores encoded data in `file_data.data`.
|
||||||
|
- `storage` stores encoded data in storage subsystem with `file-data` bucket and keeps `data` nil in `file_data` table.
|
||||||
|
- The `file_data.metadata.storage-ref-id` value points to the storage object.
|
||||||
|
- `fdata/upsert!` touches a storage object from incoming metadata before it stores the new row.
|
||||||
|
- File snapshots use `file_data` for snapshot data and `file_change` for snapshot metadata.
|
||||||
@ -24,6 +24,12 @@ Variant masters are main instances and component roots. Their descendants may th
|
|||||||
|
|
||||||
Masters are not normally touched through `set-shape-attr`, but touched flags can appear on master shapes through cloning/duplication paths. `add-touched-from-ref-chain` in `app.common.logic.variants` unions touched flags from ancestors into the copy being processed, so upstream/master touched state can affect downstream switch behavior.
|
Masters are not normally touched through `set-shape-attr`, but touched flags can appear on master shapes through cloning/duplication paths. `add-touched-from-ref-chain` in `app.common.logic.variants` unions touched flags from ancestors into the copy being processed, so upstream/master touched state can affect downstream switch behavior.
|
||||||
|
|
||||||
|
## Swap slots and positional matching
|
||||||
|
|
||||||
|
- A swap slot (stored via `ctk/set-swap-slot`, a `:touched` group `swap-slot-<uuid>`) marks a copy sub-head that was SWAPPED to another component; `compare-children` then pairs it to the main child by slot instead of by `shape-ref`.
|
||||||
|
- Copy sub-heads without a slot are paired to main children by `shape-ref` (seek, not index). `find-near-match` (positional) is only a validator/repair heuristic; validity requires membership of the ref among the near-main parent's children, not index equality (`mem:common/file-change-validation-migration-subtleties`).
|
||||||
|
- Copy child ORDER converges to the main's via the async sync (`moved` branch of `compare-children`); local code must never reorder copy children directly (guards in `:mov-objects`/`:reorder-children`).
|
||||||
|
|
||||||
## Cloning paths
|
## Cloning paths
|
||||||
|
|
||||||
`make-component-instance` in `app.common.types.container` produces a clean component copy through `update-new-shape`, dissociating attrs such as `:touched`, `:variant-id`, and `:variant-name` on cloned shapes.
|
`make-component-instance` in `app.common.types.container` produces a clean component copy through `update-new-shape`, dissociating attrs such as `:touched`, `:variant-id`, and `:variant-name` on cloned shapes.
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
## Stable namespace map
|
## Stable namespace map
|
||||||
|
|
||||||
- `app.common.data` and `app.common.data.macros`: generic data helpers and performance macros that do not depend on Penpot domain entities.
|
- `app.common.data` and `app.common.data.macros`: generic data helpers and performance macros that do not depend on Penpot domain entities.
|
||||||
- `app.common.types.*`: shared shape/file/page/component/token data types, schemas, predicates, and entity-local operations. `app.common.types.nitrate-permissions` contains shared fail-closed Nitrate organization/team permission rules.
|
- `app.common.types.*`: shared shape/file/page/component/token data types, schemas, predicates, and entity-local operations. `app.common.types.organization` contains organization schemas, `apply-organization`, and fail-closed organization/team permission rules (`allowed?`, `can-send-invitations?`).
|
||||||
- `app.common.files.*`: file-level operations, shape tree helpers, change application, migrations, validation, and undo/redo-related logic.
|
- `app.common.files.*`: file-level operations, shape tree helpers, change application, migrations, validation, and undo/redo-related logic.
|
||||||
- `app.common.logic.*`: higher-level workflows/algorithms over files, shapes, components, variants, libraries, tokens, etc.
|
- `app.common.logic.*`: higher-level workflows/algorithms over files, shapes, components, variants, libraries, tokens, etc.
|
||||||
- `app.common.geom.*`: geometry helpers and transformations.
|
- `app.common.geom.*`: geometry helpers and transformations.
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
- `set-shape-attr` treats `:position-data` as derived and never touched. Geometry/content-path changes use approximate equality; geometry differences under about 1px can be ignored for touched purposes.
|
- `set-shape-attr` treats `:position-data` as derived and never touched. Geometry/content-path changes use approximate equality; geometry differences under about 1px can be ignored for touched purposes.
|
||||||
- Width/height are excluded from the `is-geometry?` branch in `set-shape-attr`; do not assume all geometry-group attrs follow identical ignore-geometry behavior.
|
- Width/height are excluded from the `is-geometry?` branch in `set-shape-attr`; do not assume all geometry-group attrs follow identical ignore-geometry behavior.
|
||||||
- `process-touched-change` marks the owning component modified when a touched shape belongs to a main instance; component-data changes can come from shape ops through this second pass.
|
- `process-touched-change` marks the owning component modified when a touched shape belongs to a main instance; component-data changes can come from shape ops through this second pass.
|
||||||
|
- Copy structure is guarded at change application: `:mov-objects` (`is-valid-move?`) and `:reorder-children` both refuse to alter children of shapes inside component copies unless the change carries `allow-altering-copies` (sync/swap flows set it). New structural change types must follow the same rule.
|
||||||
|
- `cls/generate-delete-shapes` propagates deletions from INSIDE a component main to the copy shapes referencing them (transitively, all pages of the file) so no dangling `shape-ref`s remain; skipped when the main root itself is deleted (copies then resolve into the deleted component) and for `allow-altering-copies` flows (swap replaces the shape; sync reconciles).
|
||||||
|
|
||||||
## Shape tree edits
|
## Shape tree edits
|
||||||
|
|
||||||
@ -19,6 +21,7 @@
|
|||||||
- Full referential/semantic validation currently runs only when file features contain `"components/v2"`.
|
- Full referential/semantic validation currently runs only when file features contain `"components/v2"`.
|
||||||
- Validation starts at root plus orphan shapes, then validates component records. `validate-file!` raises `:validation :referential-integrity` with collected details.
|
- Validation starts at root plus orphan shapes, then validates component records. `validate-file!` raises `:validation :referential-integrity` with collected details.
|
||||||
- `repair-file` does not mutate data directly; it reduces validation errors into redo changes using `changes-builder`. Callers must apply or persist those changes.
|
- `repair-file` does not mutate data directly; it reduces validation errors into redo changes using `changes-builder`. Callers must apply or persist those changes.
|
||||||
|
- `:missing-slot` fires only for a REAL swap: a copy sub-head whose `shape-ref` is no longer a child of the near main parent. A pure positional mismatch (ref still a sibling elsewhere) is a reorder — valid, realigned by the async component sync; do not "repair" it by assigning swap slots (a slot freezes the child out of normal sync). `fix-missing-swap-slots` (migration 0019) follows the same membership rule.
|
||||||
|
|
||||||
## Migrations
|
## Migrations
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,9 @@
|
|||||||
## Grid assignment
|
## Grid assignment
|
||||||
|
|
||||||
- Grid `assign-cells` ensures at least one column and row, skips absolute-position children, creates non-tracked rows/cols when children exceed tracked cells, and asserts that assigned cells do not overlap.
|
- Grid `assign-cells` ensures at least one column and row, skips absolute-position children, creates non-tracked rows/cols when children exceed tracked cells, and asserts that assigned cells do not overlap.
|
||||||
|
- `position-absolute?` counts HIDDEN shapes as absolute: hiding a grid child frees its cell on the next `assign-cells`.
|
||||||
|
- `reorder-grid-children` rewrites the parent's `:shapes` to the REVERSE of the sorted cell order, but children with no cell (hidden/absolute) keep their original index — do not "fix" this into moving them to an end; that broke copy/main positional slot alignment (referential-integrity crash).
|
||||||
|
- The `:reorder-children` change it emits is refused on parents inside component copies unless `allow-altering-copies` (same rule as `:mov-objects`); `pcb/reorder-grid-children` also skips copy grids producer-side. Copy child order is owned by the component sync engine.
|
||||||
- Grid deassignment removes cells for shapes that are no longer direct children or have become absolute-positioned.
|
- Grid deassignment removes cells for shapes that are no longer direct children or have become absolute-positioned.
|
||||||
- Auto-positioning is not just sorting: some auto cells are converted to manual when empty/manual/span state would break the auto sequence, then auto single-span items can be compacted.
|
- Auto-positioning is not just sorting: some auto cells are converted to manual when empty/manual/span state would break the auto sequence, then auto single-span items can be compacted.
|
||||||
- `fix-overlaps` is marked dev-only and removes one overlapping cell, preferring empty cells first. Avoid depending on it as normal production repair.
|
- `fix-overlaps` is marked dev-only and removes one overlapping cell, preferring empty cells first. Avoid depending on it as normal production repair.
|
||||||
@ -22,23 +22,24 @@ You are working on the GitHub project `penpot/penpot`, a monorepo.
|
|||||||
- Align `let` binding values: when a `let` form has multiple bindings spanning
|
- Align `let` binding values: when a `let` form has multiple bindings spanning
|
||||||
several lines, align the value forms to the same column with spaces.
|
several lines, align the value forms to the same column with spaces.
|
||||||
- If you introduce delimiter errors (mismatched parens/brackets) in Clojure/CLJS files,
|
- If you introduce delimiter errors (mismatched parens/brackets) in Clojure/CLJS files,
|
||||||
fix them with `tools/paren-repair.bb` BEFORE running lint/format checks.
|
fix them with `scripts/paren-repair` BEFORE running lint/format checks.
|
||||||
See `mem:tools/paren-repair` for usage.
|
See `mem:scripts/paren-repair` for usage.
|
||||||
- Never run anything that destroys data without explicit permission, including `drop-devenv`, `docker compose down -v`, `docker volume rm ...`. The user's real work lives in the volumes of the shared infra.
|
- Never run anything that destroys data without explicit permission, including `drop-devenv`, `docker compose down -v`, `docker volume rm ...`. The user's real work lives in the volumes of the shared infra.
|
||||||
|
|
||||||
# Project modules
|
# Project modules
|
||||||
|
|
||||||
This is a monorepo. Principles that apply to one module do *not* generally apply to others. Do not make assumptions.
|
This is a monorepo. Principles that apply to one module do *not* generally apply to others. Do not make assumptions.
|
||||||
|
|
||||||
- `frontend/`: ClojureScript + SCSS SPA/design editor.
|
- `frontend/`: ClojureScript + SCSS SPA/design editor; core conventions: `mem:frontend/core`.
|
||||||
- `backend/`: JVM Clojure HTTP/RPC server with PostgreSQL, Redis, storage, mail, and workers.Runtime services and the task-queue vs Pub/Sub topology that constrains horizontal scaling: `mem:prod-infra/core`.
|
- `backend/`: JVM Clojure HTTP/RPC server with PostgreSQL, Redis, storage, mail, and workers; core conventions: `mem:backend/core`. Runtime services and the task-queue vs Pub/Sub topology that constrains horizontal scaling: `mem:prod-infra/core`.
|
||||||
- `common/`: shared CLJC data types, geometry, schemas, file/change logic, and utilities.
|
- `common/`: shared CLJC data types, geometry, schemas, file/change logic, and utilities; core conventions: `mem:common/core`.
|
||||||
- `render-wasm/`: Rust -> WebAssembly Skia renderer consumed by frontend.
|
- `render-wasm/`: Rust -> WebAssembly Skia renderer consumed by frontend; core conventions: `mem:render-wasm/core`.
|
||||||
- `exporter/`: ClojureScript/Node headless Playwright SVG/PDF export.
|
- `exporter/`: ClojureScript/Node headless Playwright SVG/PDF export; core conventions: `mem:exporter/core`.
|
||||||
- `mcp/`: TypeScript Model Context Protocol integration.
|
- `mcp/`: TypeScript Model Context Protocol integration; core conventions: `mem:mcp/core`.
|
||||||
- `plugins/`: TypeScript plugin runtime/examples and Plugin API types.
|
- `plugins/`: TypeScript plugin runtime/examples and Plugin API types; core conventions: `mem:plugins/core`.
|
||||||
- `library/`: design library workflows.
|
- `library/`: design library workflows; core conventions: `mem:library/core`.
|
||||||
- `docs/`: documentation site.
|
- `docs/`: documentation site; core workflow and conventions: `mem:docs/core`.
|
||||||
|
- `media-processor/`: TypeScript/Node.js HTTP service for image (sharp) and font (FontForge) processing; core conventions: `mem:media-processor/core`.
|
||||||
|
|
||||||
The memory is structured in a way that you can get the critical information about the
|
The memory is structured in a way that you can get the critical information about the
|
||||||
module. You can read it from `mem:<MODULE>/core`
|
module. You can read it from `mem:<MODULE>/core`
|
||||||
@ -52,20 +53,23 @@ module. You can read it from `mem:<MODULE>/core`
|
|||||||
- `experiments/` contains standalone experimental HTML/JS/scripts; treat it as non-core unless the user explicitly asks about it.
|
- `experiments/` contains standalone experimental HTML/JS/scripts; treat it as non-core unless the user explicitly asks about it.
|
||||||
- `sample_media/` contains sample image/icon media and config used as fixtures/demo material; do not infer app behavior from it.
|
- `sample_media/` contains sample image/icon media and config used as fixtures/demo material; do not infer app behavior from it.
|
||||||
|
|
||||||
# Dev tools
|
# Dev Scripts (scripts/)
|
||||||
|
|
||||||
- `tools/nrepl-eval.mjs` — Evaluate Clojure/ClojureScript code via nREPL.
|
- `scripts/nrepl-eval.mjs` — Evaluate Clojure/ClojureScript code via nREPL.
|
||||||
Supports `--backend` (port 6064) and `--frontend` (port 3447) aliases.
|
Supports `--backend` (port 6064) and `--frontend` (port 3447) aliases.
|
||||||
See `mem:tools/nrepl-eval`.
|
See `mem:scripts/nrepl-eval`.
|
||||||
- `tools/paren-repair.bb` — Fix mismatched delimiters in Clojure/CLJS files
|
- `scripts/paren-repair` — Fix mismatched delimiters in Clojure/CLJS files
|
||||||
and reformat with cljfmt. Run before lint checks when LLM edits break parens.
|
and reformat with cljfmt. Run before lint checks when LLM edits break parens.
|
||||||
See `mem:tools/paren-repair`.
|
See `mem:scripts/paren-repair`.
|
||||||
- `tools/psql` — PostgreSQL client wrapper with devenv defaults.
|
- `scripts/psql` — PostgreSQL client wrapper with devenv defaults.
|
||||||
Companion: `tools/db-schema` for DDL dumps. See `mem:tools/psql`.
|
Companion: `scripts/db-schema` for DDL dumps. See `mem:scripts/psql`.
|
||||||
- `tools/taiga.py` — Fetch public issues, user stories, and tasks from the
|
- `scripts/taiga.py` — Fetch public issues, user stories, and tasks from the
|
||||||
Penpot Taiga project without authentication. See `mem:tools/taiga`.
|
Penpot Taiga project without authentication. See `mem:scripts/taiga`.
|
||||||
- `tools/gh.py` — GitHub operations helper: list milestone issues, fetch PR
|
- `scripts/gh.py` — GitHub operations helper: list milestone issues, fetch PR
|
||||||
details, compare against CHANGES.md. Requires `gh` CLI. See `mem:tools/gh`.
|
details, compare against CHANGES.md. Requires `gh` CLI. See `mem:scripts/gh`.
|
||||||
|
- `scripts/error-reports.mjs` — Query error reports via RPC API with token
|
||||||
|
authentication. Supports list/get operations with filtering and pagination.
|
||||||
|
See `mem:scripts/error-reports`.
|
||||||
|
|
||||||
# Dependency graph
|
# Dependency graph
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,9 @@ Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Par
|
|||||||
|
|
||||||
## Worker policy
|
## Worker policy
|
||||||
|
|
||||||
Backend workers run only on ws0. `_env` gates `enable-backend-worker` on `PENPOT_BACKEND_WORKER`; ws1+ inject it as false. ws0 must be running whenever any ws1+ is running, and is the last instance to stop — `run-devenv --agentic --ws N` (N≥1) auto-starts ws0 first; `stop-devenv` refuses to stop ws0 while any ws1+ is up. Workers are pure fire-and-forget: `wrk/submit!` inserts a row into the shared Postgres `task` table and returns; RPC handlers never wait on completion and workers never publish to msgbus. The reason for "ws0 only" is avoiding multi-instance worker races (cron dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across submitters); details in `mem:prod-infra/core`.
|
Backend workers run only on ws0. `_env` gates `enable-backend-worker` on `PENPOT_BACKEND_WORKER`; ws1+ inject it as false. Workers are pure fire-and-forget: `wrk/submit!` inserts a row into the shared Postgres `task` table and returns; RPC handlers never wait on completion and workers never publish to msgbus. The reason for "ws0 only" is avoiding multi-instance worker races (cron dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across submitters); details in `mem:prod-infra/core`.
|
||||||
|
|
||||||
|
Each workspace is independent and can be started/stopped in any order. Shared infra (postgres, minio, etc.) is shut down only when no instances remain running.
|
||||||
|
|
||||||
## Port layout
|
## Port layout
|
||||||
|
|
||||||
@ -63,8 +65,8 @@ No `--delete` on the working-tree pass: gitignored caches in the workspace survi
|
|||||||
|
|
||||||
## CLI surface
|
## CLI surface
|
||||||
|
|
||||||
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet). Auto-starts ws0 first when the target is ws1+ and ws0 is not yet up.
|
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet).
|
||||||
- `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` (N≥1) stops just that workspace. `--ws 0` or no flag stops ws0 + shared infra, refused while any ws1+ is running. `--all` stops every ws highest-first then ws0, then infra.
|
- `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` stops just that workspace. `--ws 0` or no flag stops ws0; shared infra shuts down only if no other instances remain. `--all` stops every ws highest-first then ws0, then infra.
|
||||||
- `run-devenv`: legacy alias, ws0 non-agentic attached.
|
- `run-devenv`: legacy alias, ws0 non-agentic attached.
|
||||||
- `attach-devenv [--ws main|0|wsN|N]`: pure attach. Fails fast if instance/session missing.
|
- `attach-devenv [--ws main|0|wsN|N]`: pure attach. Fails fast if instance/session missing.
|
||||||
- `run-devenv-shell [--instance 0|wsN|N] [cmd...]`: bash in target instance. (`--instance` flag not yet renamed to `--ws`.)
|
- `run-devenv-shell [--instance 0|wsN|N] [cmd...]`: bash in target instance. (`--instance` flag not yet renamed to `--ws`.)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Docs Workflow
|
# Docs
|
||||||
|
|
||||||
`docs/`: Penpot documentation site; Eleventy.
|
`docs/`: Penpot documentation site; Eleventy.
|
||||||
|
|
||||||
@ -5,9 +5,10 @@
|
|||||||
## Layout and commands
|
## Layout and commands
|
||||||
|
|
||||||
- Source: `exporter/src/`; config: `deps.edn`, `shadow-cljs.edn`, `package.json`; runtime helpers/assets: `vendor/`, `scripts/`.
|
- Source: `exporter/src/`; config: `deps.edn`, `shadow-cljs.edn`, `package.json`; runtime helpers/assets: `vendor/`, `scripts/`.
|
||||||
- From `exporter/`: setup `./scripts/setup`; watch `pnpm run watch` or `pnpm run watch:app`; production build `pnpm run build`; lint `pnpm run lint`; format check/fix `pnpm run check-fmt` / `pnpm run fmt`.
|
- From `exporter/`: setup `./scripts/setup`; watch `pnpm run watch` or `pnpm run watch:app`; production build `pnpm run build`; test bundle `pnpm run build:test`; tests `pnpm run test` or `pnpm run test:quiet`; lint `pnpm run lint:clj`; format check/fix `pnpm run check-fmt:clj` / `pnpm run fmt:clj`.
|
||||||
- Because exporter consumes `common/`, shared file/shape/model changes may need exporter verification even when the immediate change is not under `exporter/`.
|
- Because exporter consumes `common/`, shared file/shape/model changes may need exporter verification even when the immediate change is not under `exporter/`.
|
||||||
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
|
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
|
||||||
|
- Exporter test conventions and CI: `mem:exporter/testing`.
|
||||||
|
|
||||||
## HTTP and browser pool
|
## HTTP and browser pool
|
||||||
|
|
||||||
|
|||||||
16
.serena/memories/exporter/testing.md
Normal file
16
.serena/memories/exporter/testing.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Exporter Testing
|
||||||
|
|
||||||
|
- READ `mem:testing` first.
|
||||||
|
- Tests use `cljs.test` and live under `exporter/test/exporter_tests/`.
|
||||||
|
- Register every test namespace in `exporter-tests.runner`.
|
||||||
|
- From `exporter/`: `pnpm run build:test` builds the Node test bundle without running tests.
|
||||||
|
- From `exporter/`: `pnpm run test` builds and runs tests with full output.
|
||||||
|
- From `exporter/`: `pnpm run test:quiet` builds and runs tests with reduced build output.
|
||||||
|
- After `build:test`, reuse the compiled bundle with `node target/tests/test.js`.
|
||||||
|
- For iterative focused runs, build once and reuse the compiled bundle.
|
||||||
|
- Focus a test namespace with `node target/tests/test.js --focus exporter-tests.renderer-svg-test`.
|
||||||
|
- Focus a test var with `node target/tests/test.js --focus exporter-tests.renderer-svg-test/creates-the-correct-gradient-element`.
|
||||||
|
- Set app log level by appending `--log-level warn` (or `trace|debug|info|warn|error`).
|
||||||
|
- `test:quiet` accepts forwarded options but rebuilds the bundle; prefer the direct runner after `build:test` for focused runs.
|
||||||
|
- From `exporter/`: `pnpm run check-fmt:clj` checks ClojureScript formatting.
|
||||||
|
- From `exporter/`: `pnpm run lint:clj` runs ClojureScript linting.
|
||||||
152
.serena/memories/frontend/composable-component-tests.md
Normal file
152
.serena/memories/frontend/composable-component-tests.md
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
# Composable component tests
|
||||||
|
|
||||||
|
A framework concept for systematically testing Penpot's component subsystem
|
||||||
|
(synchronisation/propagation, swaps, variant switches, nesting, overrides), implemented in TWO test
|
||||||
|
suites that share the principles below:
|
||||||
|
|
||||||
|
1. **ClojureScript suite** — in the frontend test tree (`frontend/test/frontend_tests/
|
||||||
|
composable_tests/`), driving a minimally-assembled real app headlessly. The original.
|
||||||
|
2. **TypeScript suite** — a Penpot plugin (`plugins/apps/composable-test-suite/`), driving the FULL
|
||||||
|
production app end-to-end through the Plugin API, with a slightly more elaborate set of
|
||||||
|
abstractions. Runs interactively (panel), remotely (Playwright), and headlessly in CI. Its
|
||||||
|
README is the authoritative operational reference.
|
||||||
|
|
||||||
|
## Shared core idea
|
||||||
|
A test is a **composition of operations** over a starting configuration, plus assertions. You
|
||||||
|
describe a test as data (a setup + a sequence of operations) rather than writing bespoke imperative
|
||||||
|
code, and coverage grows by COMPOSITION: a new variation is one combinator wrapped around existing
|
||||||
|
pieces, not a copied test. Choice points (one-of alternatives, optional steps) EXPAND the
|
||||||
|
composition into a full sweep of variants — one written case stands for a whole matrix of concrete
|
||||||
|
tests.
|
||||||
|
|
||||||
|
## Shared principles
|
||||||
|
- **Every producing object is the accessor interface to what it produces downstream.** An
|
||||||
|
operation — and related objects such as content-creation strategies — is not merely an action:
|
||||||
|
the SAME object instance the case holds is the typed interface through which everything it
|
||||||
|
created or changed is later retrieved, checked, and asserted, parameterized by the situation. A
|
||||||
|
foundation operation exposes accessors for the participants it built; an edit operation exposes
|
||||||
|
its dual check (`assertHasChangedProperty` / `has-property-of`); a choice is recovered by asking
|
||||||
|
the one-of object (`getChoice`/`get-choice`); "did this step run" is asked of the step
|
||||||
|
(`wasApplied`/`applied?`). NEVER reach into a situation (or the document) for something an
|
||||||
|
upstream object produced — ask the producer. This is what keeps sweeps sound (object identity
|
||||||
|
ties the question to the exact node that ran) and what keeps retrieval logic in exactly one
|
||||||
|
place. Particularly explicit in the TS OOP implementation, where these accessors are methods on
|
||||||
|
the operation/strategy classes; repeatedly violating it (reading the document directly,
|
||||||
|
duplicating retrieval) was the most common review correction while building the suites.
|
||||||
|
- **Operations are data with identity.** Each operation node has a unique id at construction and
|
||||||
|
records what it did under that id; interrogation is by identity. Bind an operation to a value
|
||||||
|
ONCE and reuse it in the composition and in every query about it.
|
||||||
|
- **Drive the real production pipeline.** Operations route through genuine Penpot logic — real
|
||||||
|
change functions / real workspace events / the real Plugin API, never raw field writes — so the
|
||||||
|
production watcher's AUTOMATIC propagation is what's under test.
|
||||||
|
- **Roles, not internals.** A starting configuration names its participants (roles). Role→id
|
||||||
|
capture happens when the configuration is built; operation TARGETS resolve at apply-time and may
|
||||||
|
be re-bound, so an operation targeting a role follows it as state-building ops re-point it —
|
||||||
|
which lets a single operation be swept across depth.
|
||||||
|
- **Enumeration is authored, not exhaustive.** Compose only VALID cases, so outcomes are just
|
||||||
|
pass / fail / error — no not-applicable cells.
|
||||||
|
- **Naming discipline.** Penpot domain nouns ("component", "variant") must not name framework
|
||||||
|
abstractions; an operation may name the domain ACTION it performs.
|
||||||
|
- **Operator algebra** (same in both suites): sequence (cartesian product of the steps' variants),
|
||||||
|
one-of (union, choice recorded), optional(X) = one-of([X, skip]), inline assertion ops, trailing
|
||||||
|
asserters.
|
||||||
|
- **Case authoring:** a case carries a CamelCase identifier and a plain-terms description in three
|
||||||
|
parts — situation setup, actions/variations, asserted requirement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# ClojureScript suite (frontend test tree)
|
||||||
|
|
||||||
|
Test-only `.cljs` code in the frontend test tree (nothing "common" about it). A **situation** =
|
||||||
|
the in-memory file value + named roles + `:vars` + an ordered applied-log. Operations are records
|
||||||
|
implementing `IOperation`/`apply-to` (`apply` collides with core). Assertions = inline `Test` ops
|
||||||
|
and/or a trailing asserter; the runner makes no judgment. Failures carry `describe-applied` (the
|
||||||
|
transcript), which is what makes a failing variant in a sweep identifiable.
|
||||||
|
|
||||||
|
Layout: `core.cljs` (the domain-agnostic engine: situation, identity/transcript, roles/targets,
|
||||||
|
operators, runners), `comp/setups.cljs` (setups + role accessors), `comp/nodes.cljs` (the component
|
||||||
|
operations and their check duals), `interpreter.cljs` (runs cases against the real frontend),
|
||||||
|
`comp/sync_test.cljs` (the cases; registered in `frontend_tests/runner.cljs`). Case letters B..N;
|
||||||
|
the sweeps (K: depth × edit-precedence; L: swaps; M: variant switches; N: rotated-instance
|
||||||
|
geometry, on the #10109 fix branch until merged) are the flagship pattern — read them before
|
||||||
|
writing a new sweep.
|
||||||
|
|
||||||
|
**Scenario lineage model** (behind the sweeps): scenario ops track named component lineages as
|
||||||
|
objects under `:vars`, each holding the FIXED deepest origin (`:remote-*`), the ADVANCING outer
|
||||||
|
main (`:main-*`), and per-nesting-level data whose `:nested-head` (the deepest instance at that
|
||||||
|
level, found by descending the `:shape-ref` chain — matching chain MEMBERSHIP, not terminus) is
|
||||||
|
the swap/switch target, anchored by its swap-stable parent. Nesting seeks the FIXED origin, not
|
||||||
|
the advancing main — that is what makes each level's `:nested-head` land on the deepest instance.
|
||||||
|
A variant nesting re-points the lineage's remote to the chosen member. Construction lesson:
|
||||||
|
cross-level propagation requires progressively NESTED levels (one variant + plain wraps); sibling
|
||||||
|
nestings do not propagate between each other.
|
||||||
|
|
||||||
|
**Interpreter:** installs the situation's files into the global `st/state` (aux files tagged
|
||||||
|
`:library-of`), starts the real `watch-component-changes` (+ harness `watch-undo-stack`), maps
|
||||||
|
event-ops to REAL workspace events (`dwsh/update-shapes`, `dwl/component-swap`,
|
||||||
|
`dwv/variants-switch`, `dwt/increase-rotation` — which runs the `check-delta` placement
|
||||||
|
classification — `dwt/update-dimensions`, `dwu/undo`, `dwl/sync-file`, …) and runs sync-ops'
|
||||||
|
`apply-to` against the live store file; awaits settlement (idle-gap heuristic + per-op grace) and
|
||||||
|
re-reads `:file` each step so the shared accessors keep working.
|
||||||
|
STORE-SWAP IMMUNITY: other test namespaces `set!` `st/state`/`st/stream` and never restore, while
|
||||||
|
the `app.main.refs` lenses stay bound to the ORIGINAL atoms — propagation then dies silently. The
|
||||||
|
interpreter captures the atoms at namespace-load time and re-`set!`s them per variant.
|
||||||
|
|
||||||
|
Running: `cd frontend && pnpm run build:test`, then
|
||||||
|
`node target/tests/test.js --focus frontend-tests.composable-tests.comp.sync-test`
|
||||||
|
(var-level focus for one case).
|
||||||
|
|
||||||
|
**Fidelity warning:** the harness drives a MINIMALLY-ASSEMBLED app — only some
|
||||||
|
`initialize-workspace` subscriptions are wired. Risk = SILENT UNDER-WIRING (e.g. undo needs the
|
||||||
|
harness `watch-undo-stack`). When a case needs app behaviour beyond a raw edit, check for an
|
||||||
|
unwired subscription and verify by PROBING store state, not by trusting a green assertion.
|
||||||
|
|
||||||
|
**Caveats:** inline `Test` exceptions are UNCAUGHT on the frontend (crash the runner — assert in
|
||||||
|
the trailing asserter). `(optional (in-sequence …))` is not flattened for the interpreter — use
|
||||||
|
independent optionals. The Serena/clj-kondo cache for `nodes.cljs` goes stale (phantom symbols) —
|
||||||
|
trust the build. Cross-namespace global-state leaks land in this suite first; suspect them before
|
||||||
|
the framework on inexplicable full-run-only failures. Case H's `sync-file` schedules a delayed RPC
|
||||||
|
that fails headless (benign; absorbed by per-op grace).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# TypeScript suite (the plugin) — full e2e
|
||||||
|
|
||||||
|
`plugins/apps/composable-test-suite/` — same principles against the FULL production app through the
|
||||||
|
Plugin API (real frontend, real propagation). Continuation of the CLJS suite per issue #10584.
|
||||||
|
Operational details (build/run, connect URL, remote control, reading logs, auto-reload, CI): the
|
||||||
|
plugin README.
|
||||||
|
|
||||||
|
Distinguishing abstractions (the OOP articulation of the shared principles):
|
||||||
|
- `TestCase {identifier, description, operation}` with the three-part description mandated in the
|
||||||
|
constructor docstring.
|
||||||
|
- The accessor-interface principle is class-level: foundation operations (e.g.
|
||||||
|
`OpCreateSimpleComponentWithCopy`) expose the roles they build; **content-creation strategies**
|
||||||
|
(pluggable: what content a foundation builds around) expose accessors for the content they
|
||||||
|
created; edit operations expose their checks (`OpChangeProperty.assertHasChangedProperty`);
|
||||||
|
`OpOneOf`/`OpOptional` are queried for what ran. Tests never grope the document for something a
|
||||||
|
producer can be asked for.
|
||||||
|
- `ShapeProp` model: property duals with numeric tolerance; rotation is a writable attr, height
|
||||||
|
goes via resize (readonly in the Plugin API).
|
||||||
|
- `TestSuite` enumerates cases into a `TestTree` with stable per-test ids;
|
||||||
|
`run(ids, TestRunObserver)` is the ONLY output channel — the framework is UI-free by
|
||||||
|
construction. `plugin.ts` (panel adapter), `main.ts` (panel UI) and `src/ci/headless.ts`
|
||||||
|
(CI adapter) are three thin consumers.
|
||||||
|
- Cases live in `src/composable-tests/cases/` as `case<Identifier>.ts` (e.g. `MainEditSyncs` — the
|
||||||
|
sweep that found #10109).
|
||||||
|
- Panel checkboxes carry stable DOM ids (case identifier / `Identifier-N` composites) for remote
|
||||||
|
control via Playwright; recipe in the README.
|
||||||
|
|
||||||
|
## CI
|
||||||
|
Headless per-PR gate: `.github/workflows/tests-composable-suite.yml` runs
|
||||||
|
`pnpm --filter composable-test-suite run test:ci` — mocked backend (frontend e2e static server +
|
||||||
|
Playwright RPC fixtures, no backend/login), the in-sandbox bundle injected via `ɵloadPlugin`,
|
||||||
|
results streamed via console markers, `TEST_FILTER` by identifier substring. The mocked backend is
|
||||||
|
NOT a limitation for this suite (everything asserted is frontend store logic; empirically
|
||||||
|
confirmed against the interactive runs). Architecture mirrors `plugin-api-test-suite`'s CI driver;
|
||||||
|
the mock harness exists in THREE places that must stay in sync (provenance note in `ci/run-ci.ts`).
|
||||||
|
Details: README, "Running in CI".
|
||||||
|
|
||||||
|
## Substrate
|
||||||
|
`mem:common/test-setup`, `mem:common/component-data-model`, `mem:common/component-swap-pipeline`,
|
||||||
|
`mem:frontend/testing`.
|
||||||
@ -23,13 +23,13 @@ From `frontend/`:
|
|||||||
- JS lint currently no-ops via `pnpm run lint:js`.
|
- JS lint currently no-ops via `pnpm run lint:js`.
|
||||||
- SCSS lint: `pnpm run lint:scss`.
|
- SCSS lint: `pnpm run lint:scss`.
|
||||||
- Format checks: `pnpm run check-fmt:clj`, `pnpm run check-fmt:js`, `pnpm run check-fmt:scss`.
|
- Format checks: `pnpm run check-fmt:clj`, `pnpm run check-fmt:js`, `pnpm run check-fmt:scss`.
|
||||||
- Format fix: `pnpm run fmt`, or targeted `fmt:clj` / `fmt:js` / `fmt:scss`.
|
- Format fix: `pnpm run fmt`, or targeted `fmt:clj` / `fmt:js` / `fmt:scss`. After running `fmt:*`, `check-fmt:*` is redundant.
|
||||||
- Translation formatting after i18n edits: `pnpm run translations`.
|
- Translation formatting after i18n edits: `pnpm run translations`.
|
||||||
|
|
||||||
**Before linting:** if delimiter errors are suspected (after LLM edits, or
|
**Before linting:** if delimiter errors are suspected (after LLM edits, or
|
||||||
lint/compiler reports syntax errors), run `tools/paren-repair.bb` on the
|
lint/compiler reports syntax errors), run `scripts/paren-repair` on the
|
||||||
affected files first. Delimiter errors produce misleading linter output.
|
affected files first. Delimiter errors produce misleading linter output.
|
||||||
See `mem:tools/paren-repair`.
|
See `mem:scripts/paren-repair`.
|
||||||
|
|
||||||
## Focused memory routing
|
## Focused memory routing
|
||||||
|
|
||||||
|
|||||||
@ -11,10 +11,10 @@ The latter is needed because syntax errors in parentheses give an uninformative
|
|||||||
tool can often find the exact location of such errors.
|
tool can often find the exact location of such errors.
|
||||||
|
|
||||||
When delimiter errors are detected (typically from lint or compiler output),
|
When delimiter errors are detected (typically from lint or compiler output),
|
||||||
fix the affected files with `tools/paren-repair.bb`. The `clj_check_parentheses`
|
fix the affected files with `scripts/paren-repair`. The `clj_check_parentheses`
|
||||||
MCP tool can also pinpoint the error location when available, but it is not
|
MCP tool can also pinpoint the error location when available, but it is not
|
||||||
required — standard build errors are usually enough.
|
required — standard build errors are usually enough.
|
||||||
See `mem:tools/paren-repair`.
|
See `mem:scripts/paren-repair`.
|
||||||
|
|
||||||
## Runtime patching with `set!`
|
## Runtime patching with `set!`
|
||||||
|
|
||||||
|
|||||||
100
.serena/memories/media-processor/core.md
Normal file
100
.serena/memories/media-processor/core.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# Media Processor
|
||||||
|
|
||||||
|
Stateless HTTP service for Penpot image and font processing. Handles image info extraction, thumbnail generation (sharp), and font conversion (FontForge, woff-tools).
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- Language: TypeScript
|
||||||
|
- Runtime: Node.js
|
||||||
|
- Framework: Express
|
||||||
|
- Image processing: sharp (libvips)
|
||||||
|
- Font processing: FontForge (TTF/OTF), sfnt2woff, woff2_decompress
|
||||||
|
- Upload handling: multer (hybrid storage: memory for small, disk for large)
|
||||||
|
- Logging: pino (with optional Loki transport)
|
||||||
|
- Config validation: Zod
|
||||||
|
- Testing: Vitest
|
||||||
|
- Package Manager: pnpm
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
media-processor/
|
||||||
|
├── src/
|
||||||
|
│ ├── index.ts # Express app setup, routes, middleware
|
||||||
|
│ ├── config.ts # Zod-validated env config, HKDF key derivation
|
||||||
|
│ ├── types.ts # TypeScript type definitions
|
||||||
|
│ ├── upload.ts # Multer configuration, getFileBuffer helper
|
||||||
|
│ ├── upload-storage.ts # Hybrid storage engine (memory < threshold, disk >= threshold)
|
||||||
|
│ ├── logger.ts # Pino logger setup
|
||||||
|
│ ├── middleware/
|
||||||
|
│ │ ├── auth.ts # Timing-safe shared key authentication
|
||||||
|
│ │ ├── error-handler.ts # ProcessingError class, centralized error handling
|
||||||
|
│ │ └── timeout.ts # Request timeout middleware
|
||||||
|
│ ├── routes/
|
||||||
|
│ │ ├── health.ts # GET /api/health
|
||||||
|
│ │ ├── image.ts # POST /api/image/info, /api/image/thumbnail
|
||||||
|
│ │ └── font.ts # POST /api/font/convert
|
||||||
|
│ └── services/
|
||||||
|
│ ├── image.ts # sharp-based image info/thumbnail generation
|
||||||
|
│ ├── font.ts # FontForge/woff-tools font conversion
|
||||||
|
│ └── errors.ts # throwValidation, throwRestriction, throwProcessing
|
||||||
|
├── test/ # Vitest test files
|
||||||
|
├── vitest.config.ts # Test configuration
|
||||||
|
├── tsconfig.json # TypeScript configuration
|
||||||
|
├── esbuild.config.mjs # Build configuration
|
||||||
|
└── package.json # Dependencies and scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Conventions
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
- Requests authenticated via `x-shared-key` header using timing-safe comparison
|
||||||
|
- When no key configured, all requests rejected with 403
|
||||||
|
- Key derived from `PENPOT_SECRET_KEY` via HKDF (blake2b512) or set directly via `PENPOT_MEDIA_PROCESSOR_SHARED_KEY`
|
||||||
|
|
||||||
|
### Resource Limits
|
||||||
|
- Image: max pixels, max width/height enforced before processing
|
||||||
|
- Font: prlimit wraps FontForge processes with memory (AS) and CPU time limits
|
||||||
|
- Concurrency: p-queue limits concurrent requests (default 10)
|
||||||
|
- Upload: hybrid storage — memory for files < 10MB, disk for larger; configurable via `PENPOT_MEDIA_PROCESSOR_MEMORY_THRESHOLD`
|
||||||
|
- Max file size: configurable (default 350MB)
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- `throwValidation(code, hint)` — 400 errors for invalid input
|
||||||
|
- `throwRestriction(code, hint)` — 413 errors for resource limits exceeded
|
||||||
|
- `throwProcessing(code, hint)` — 503 errors for processing failures (e.g., resource limit kills)
|
||||||
|
|
||||||
|
### Image Processing
|
||||||
|
- EXIF orientation applied before dimension validation and thumbnail generation
|
||||||
|
- sharp caching disabled to prevent unbounded memory growth
|
||||||
|
- `withoutEnlargement: true` prevents upscaling small images
|
||||||
|
|
||||||
|
### Font Conversion
|
||||||
|
- Supported formats: TTF, OTF, WOFF, WOFF2
|
||||||
|
- SFNT type detected via magic bytes (0x4f54544f = OTF, 0x00010000 = TTF)
|
||||||
|
- Temp files cleaned up in finally blocks (best-effort)
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
All commands run from `media-processor/` directory:
|
||||||
|
|
||||||
|
- `pnpm run test` — Run Vitest test suite
|
||||||
|
- `pnpm run types:check` — TypeScript type checking (tsc --noEmit)
|
||||||
|
- `pnpm run fmt` — Format code with Prettier
|
||||||
|
- `pnpm run fmt:check` — Check formatting without modifying
|
||||||
|
- `pnpm run build` — Build for production (esbuild)
|
||||||
|
- `pnpm run start:dev` — Start development server (tsx)
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
- Exposed port: 6065 (configurable via `PENPOT_MEDIA_PROCESSOR_PORT`)
|
||||||
|
- Must be deployed on internal Docker network only (not public-facing)
|
||||||
|
- Backend communicates via `PENPOT_MEDIA_PROCESSING_SERVICE_URI`
|
||||||
|
|
||||||
|
## Testing Principles
|
||||||
|
|
||||||
|
Cross-cutting testing principles and anti-patterns: `mem:testing`.
|
||||||
|
|
||||||
|
- Run `pnpm run test` after changes
|
||||||
|
- Run `pnpm run types:check` after TypeScript changes
|
||||||
|
- Run `pnpm run fmt:check` before commits
|
||||||
@ -6,7 +6,7 @@ Backend (`app.config`, `PENPOT_*` env vars) is parameterized; deployments choose
|
|||||||
|
|
||||||
- **PostgreSQL**: durable store. Profiles, teams, files, sessions, audit, `storage_object` metadata, the `task` queue, `scheduled_task` cron registry, migrations. File-data also lives here when the file-data backend is `legacy-db`/`db`. One shared DB across all backends.
|
- **PostgreSQL**: durable store. Profiles, teams, files, sessions, audit, `storage_object` metadata, the `task` queue, `scheduled_task` cron registry, migrations. File-data also lives here when the file-data backend is `legacy-db`/`db`. One shared DB across all backends.
|
||||||
- **Redis (Valkey-compatible)**: per-backend message bus and cache. Concrete uses: msgbus Pub/Sub for collaborative-editing broadcasts and team/profile-org notifications fired by RPC handlers (`app.rpc.notifications`, `files_update`, `teams`, `websocket`); file-summary cache gated by `enable-redis-cache`; rate-limit counters; and the dispatcher→runner work hand-off list `penpot.worker.queue:<tenant>:<queue>`. `PENPOT_REDIS_URI`.
|
- **Redis (Valkey-compatible)**: per-backend message bus and cache. Concrete uses: msgbus Pub/Sub for collaborative-editing broadcasts and team/profile-org notifications fired by RPC handlers (`app.rpc.notifications`, `files_update`, `teams`, `websocket`); file-summary cache gated by `enable-redis-cache`; rate-limit counters; and the dispatcher→runner work hand-off list `penpot.worker.queue:<tenant>:<queue>`. `PENPOT_REDIS_URI`.
|
||||||
- **Object storage**: backends `:s3` and `:fs`. S3 in prod; devenv uses MinIO. Holds uploaded media, file-data when the file-data backend is `storage`, exports. Backend-side details (resolve, dedup, bucket set, file-data backends): `mem:backend/http-storage-filedata-subtleties`.
|
- **Object storage**: backends `:s3` and `:fs`. S3 in prod; devenv uses MinIO. Holds uploaded media, file-data when the file-data backend is `storage`, exports. Backend-side details (resolve, dedup, bucket set, object lifecycle, and file-data backends): `mem:backend/storage`.
|
||||||
- **SMTP mailer**: invitations, password resets, email verification (sent via the `:sendmail` worker task).
|
- **SMTP mailer**: invitations, password resets, email verification (sent via the `:sendmail` worker task).
|
||||||
- **LDAP** (optional auth provider): helpers in `app.auth.*`, gated by `enable-login-with-ldap`.
|
- **LDAP** (optional auth provider): helpers in `app.auth.*`, gated by `enable-login-with-ldap`.
|
||||||
|
|
||||||
@ -30,4 +30,4 @@ Penpot in production lives with both: horizontal-scale deployments accept "exact
|
|||||||
## See also
|
## See also
|
||||||
|
|
||||||
- Devenv composition and the ws0-only worker placement: `mem:devenv/core`.
|
- Devenv composition and the ws0-only worker placement: `mem:devenv/core`.
|
||||||
- Storage backend resolution, dedup, file-data lifecycle: `mem:backend/http-storage-filedata-subtleties`.
|
- Storage backend resolution, dedup, bucket behavior, object lifecycle, and file-data lifecycle: `mem:backend/storage`.
|
||||||
|
|||||||
@ -17,9 +17,13 @@
|
|||||||
|
|
||||||
## Tile/render behavior
|
## Tile/render behavior
|
||||||
|
|
||||||
|
- Raster `Fill::Image`: skip `save_layer` unless the shape has an image filter; plain
|
||||||
|
Rect/Frame (no corners) also skip the container clip (`draw_image_fill` in fills.rs).
|
||||||
- Interactive transforms are distinct from viewport fast mode. `set_modifiers_start` enables fast mode and interactive transform; interactive transform still flushes each animation frame.
|
- Interactive transforms are distinct from viewport fast mode. `set_modifiers_start` enables fast mode and interactive transform; interactive transform still flushes each animation frame.
|
||||||
- During interactive transform, modifier tile invalidation is deferred to `render()` once per rAF. Outside interactive transform, `set_modifiers` rebuilds modifier tiles immediately.
|
- During interactive transform, modifier tile invalidation is deferred to `render()` once per rAF. Outside interactive transform, `set_modifiers` rebuilds modifier tiles immediately.
|
||||||
- `set_modifiers_end` disables fast/interactive state and cancels pending async render; the caller must request the final full-quality render.
|
- `set_modifiers_end` disables fast/interactive state and cancels pending async render; the caller must request the final full-quality render.
|
||||||
- Plain viewport fast mode (`options.is_viewport_interaction()`) renders from cache and does not flush target output inside `process_animation_frame`; interactive transforms do flush.
|
- Plain viewport fast mode (`options.is_viewport_interaction()`) renders from cache and does not flush target output inside `process_animation_frame`; interactive transforms do flush.
|
||||||
- Zoom changes rebuild the tile index while preserving cached tile textures. Avoid replacing that path with shallow rebuilds if blur/shadow cache preservation matters.
|
- Zoom changes rebuild the tile index while preserving cached tile textures. Avoid replacing that path with shallow rebuilds if blur/shadow cache preservation matters.
|
||||||
- Pending tile priority is intentionally reversed by pop order; check the queue construction before changing tile scheduling.
|
- Pending tile priority is intentionally reversed by pop order; check the queue construction before changing tile scheduling.
|
||||||
|
- Frames with a fill may use `render_frame_container_drop_shadow` (direct rrect +
|
||||||
|
blur saveLayer on `DropShadows`) when `uses_direct_container_drop_shadow` is true.
|
||||||
289
.serena/memories/scripts/error-reports.md
Normal file
289
.serena/memories/scripts/error-reports.md
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
# Error Reports CLI Tool
|
||||||
|
|
||||||
|
`scripts/error-reports.mjs` is a Node.js CLI tool for querying Penpot error reports via the RPC API. Provides access to error logs with filtering, pagination, and multiple output formats.
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
- Querying error reports from the database for debugging or analysis
|
||||||
|
- Filtering errors by source, kind, tenant, or backend version
|
||||||
|
- Exporting error data in JSON, NDJSON, or table format
|
||||||
|
- Computing error statistics (top signatures, version, source, audit-log kind, hourly distribution, bursts, heatmap)
|
||||||
|
- Investigating specific error reports by ID
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js with `commander` and `dotenv` packages installed (in root `package.json`)
|
||||||
|
- Running Penpot backend with error-reports RPC endpoints
|
||||||
|
- Access token with `error-reports:read` permission
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create a `.env` file in the project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PENPOT_API_URI=http://localhost:3450
|
||||||
|
PENPOT_ACCESS_TOKEN=<your-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
Grant the required permission to your access token:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE access_token
|
||||||
|
SET perms = ARRAY['error-reports:read']::text[],
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = '<token-uuid>';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs <command> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
#### `list` - List error reports with pagination and filters
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
| Flag | Description | Default |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| `-l, --limit <n>` | Max items per page (max: 200) | `50` |
|
||||||
|
| `--from <date>` | ISO timestamp — oldest boundary (items after this) | — |
|
||||||
|
| `--to <date>` | ISO timestamp — newest boundary (items before this) | — |
|
||||||
|
| `--since <date>` | ISO timestamp — explicit cursor for manual pagination | — |
|
||||||
|
| `--since-id <uuid>` | Fetch errors after this ID (cursor pagination) | — |
|
||||||
|
| `-s, --source <name>` | Filter by source (see source names below) | — |
|
||||||
|
| `-p, --profile-id <uuid>` | Filter by profile ID | — |
|
||||||
|
| `-k, --kind <kind>` | Filter by kind (string) | — |
|
||||||
|
| `-t, --tenant <tenant>` | Filter by tenant (string) | — |
|
||||||
|
| `--version <version>` | Filter by version | — |
|
||||||
|
| `--hint <text>` | Filter by hint (ILIKE match) | — |
|
||||||
|
| `-a, --all` | Fetch all pages automatically (streams output) | `false` |
|
||||||
|
| `-f, --format <type>` | Output format: `json`, `table`, or `ndjson` | `table` |
|
||||||
|
| `--normalize-hints` | Normalize hints by stripping dynamic values | `false` |
|
||||||
|
| `-o, --output <file>` | Write output to file instead of stdout | — |
|
||||||
|
| `--env <path>` | Custom .env file path | `.env` |
|
||||||
|
| `-h, --help` | Show help message | — |
|
||||||
|
|
||||||
|
**Streaming behavior:** With `--all`, output must be `ndjson` or `table`; `--all --format json` is rejected because `--all` streams output. `--all --format table` prints rows immediately. `--format ndjson` always streams one JSON object per line.
|
||||||
|
|
||||||
|
#### `get` - Get a single error report by ID
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs get [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
| Flag | Description | Required |
|
||||||
|
|------|-------------|----------|
|
||||||
|
| `--id <uuid>` | Error report ID | Yes (or --error-id) |
|
||||||
|
| `--error-id <id>` | Error report error-id | Yes (or --id) |
|
||||||
|
| `-f, --format <type>` | Output format: `json` or `table` | No (default: `table`) |
|
||||||
|
| `--env <path>` | Custom .env file path | No (default: `.env`) |
|
||||||
|
| `-h, --help` | Show help message | No |
|
||||||
|
|
||||||
|
#### `stats` - Compute error report statistics
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs stats [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
Reads from `--input <file>`, stdin (piped), or fetches from API. Computes aggregations by signature, version, source, audit-log kind, hour, optional 5-minute bursts, and optional day-of-week × hour heatmap.
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
| Flag | Description | Default |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| `--from <date>` | Start of interval (ISO timestamp) | — |
|
||||||
|
| `--to <date>` | End of interval (ISO timestamp) | — |
|
||||||
|
| `--limit <n>` | Items per page when fetching from API | `200` |
|
||||||
|
| `--input <file>` | Read from local JSON/NDJSON file instead of API | — |
|
||||||
|
| `--burst` | Detect 5-minute windows above 3× the average rate | `false` |
|
||||||
|
| `--heatmap` | Show day-of-week × hour-of-day heatmap | `false` |
|
||||||
|
| `-f, --format <type>` | Output format: `json` or `table` | `table` |
|
||||||
|
| `--env <path>` | Custom .env file path | `.env` |
|
||||||
|
|
||||||
|
## Source Names
|
||||||
|
|
||||||
|
The `--source` filter accepts these values:
|
||||||
|
|
||||||
|
- `logging`
|
||||||
|
- `audit-log`
|
||||||
|
- `rlimit`
|
||||||
|
|
||||||
|
## Hint Normalization
|
||||||
|
|
||||||
|
With `--normalize-hints` (or always in `stats`), hints are normalized by stripping dynamic values:
|
||||||
|
|
||||||
|
1. File IDs in file-id context → `<file-id>`
|
||||||
|
2. UUIDs (8-4-4-4-12 hex) → `<uuid>`
|
||||||
|
3. Numeric IDs in parentheses `(12345)` → `(<id>)`
|
||||||
|
4. Elapsed times (`7.5s`, `2m3.027s`) → `<elapsed>`
|
||||||
|
5. URIs (`https://...`) → `<uri>`
|
||||||
|
6. Unicode quotes and whitespace normalized
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### List recent errors
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --limit 10
|
||||||
|
```
|
||||||
|
|
||||||
|
### Time-range query (today)
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --from 2026-07-23T00:00:00Z --to 2026-07-23T23:59:59Z --all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stream all errors as NDJSON
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --all --format ndjson > errors.ndjson
|
||||||
|
```
|
||||||
|
|
||||||
|
### Save to file with --output
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --all --format ndjson -o errors.ndjson
|
||||||
|
./scripts/error-reports.mjs list --format json -o errors.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter by source
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --source audit-log --limit 20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter by kind
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --kind exception-page
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter by tenant
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --tenant production
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter by version
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --version 2.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search by hint (partial match)
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --hint "NullPointerException"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fetch all errors with pagination
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get specific error by ID
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs get --id 550e8400-e29b-41d4-a716-446655440000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output as JSON
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --limit 5 --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combine filters
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --source audit-log --kind exception-page --tenant production --limit 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stats with burst and heatmap analysis
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs stats --from 2026-07-23T00:00:00Z --to 2026-07-23T23:59:59Z --burst --heatmap
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stats from file
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs stats --input errors.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stats from pipe
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --all --format json | ./scripts/error-reports.mjs stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Formats
|
||||||
|
|
||||||
|
### Table (default)
|
||||||
|
Human-readable table format for terminal display. With `--all`, rows stream as they arrive.
|
||||||
|
|
||||||
|
### JSON
|
||||||
|
Single page: `{items: [...], nextSince, nextId}`. `--all` cannot be combined with `--format json`; use `--format ndjson` for streaming.
|
||||||
|
|
||||||
|
### NDJSON
|
||||||
|
One JSON object per line, always streaming. Pipe-friendly: `| jq -c '.hint'`, `| wc -l`.
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
The server returns items in **ascending** order (oldest first). Cursor pagination uses `--since` / `--since-id` to fetch the next page of newer items.
|
||||||
|
|
||||||
|
### Manual pagination
|
||||||
|
Use `--since` and `--since-id` with values from `nextSince` and `nextId` in the response:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --limit 50
|
||||||
|
# Use nextSince and nextId from response
|
||||||
|
./scripts/error-reports.mjs list --limit 50 --since "2026-01-20T10:29:00Z" --since-id "next-uuid"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic pagination
|
||||||
|
Use `--all` to fetch all pages automatically (streams output):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Time-range queries
|
||||||
|
Use `--from` and `--to` to bound the query. These map to the server's `--since` and `--until` parameters:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --from 2026-07-20T00:00:00Z --to 2026-07-23T23:59:59Z --all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key principles
|
||||||
|
|
||||||
|
- **Authentication required** - Uses access token with `error-reports:read` permission
|
||||||
|
- **API endpoint configurable** - Set via `PENPOT_API_URI` in `.env` file
|
||||||
|
- **Table is default format** - Use `--format json` for structured JSON, `--format ndjson` for streaming
|
||||||
|
- **Streaming with --all** - Items print as they arrive, no buffering. Use `--format ndjson` or `--format table`; `--all --format json` is rejected.
|
||||||
|
- **Filters are combinable** - All filter options can be used together
|
||||||
|
- **Both flag formats supported** - `--option=value` and `--option value` both work
|
||||||
|
- **Ascending order** - Server returns oldest items first (changed from DESC)
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
The tool provides helpful error messages for common issues:
|
||||||
|
|
||||||
|
- **Missing configuration**: Shows setup instructions for `.env` file
|
||||||
|
- **Authentication errors (401)**: Indicates invalid or expired token
|
||||||
|
- **Authorization errors (403)**: Indicates missing `error-reports:read` permission
|
||||||
|
- **RPC errors**: Displays error code and message from the API
|
||||||
|
|
||||||
|
## Integration with other scripts
|
||||||
|
|
||||||
|
- **jq**: Pipe NDJSON output to `jq` for further processing
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --all --format ndjson | jq -c '{id, hint}'
|
||||||
|
```
|
||||||
|
- **stats from pipe**: Fetch data once, compute stats
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --all --format ndjson | ./scripts/error-reports.mjs stats
|
||||||
|
```
|
||||||
|
- **stats from NDJSON pipe**: Works with NDJSON format too
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --all --format ndjson | ./scripts/error-reports.mjs stats
|
||||||
|
```
|
||||||
|
- **grep/search**: Filter output by specific patterns
|
||||||
|
- **--output**: Save to file without shell redirection
|
||||||
|
```bash
|
||||||
|
./scripts/error-reports.mjs list --all --format ndjson -o errors.ndjson
|
||||||
|
```
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# GitHub operations helper
|
# GitHub operations helper
|
||||||
|
|
||||||
`tools/gh.py` is a multi-purpose CLI for querying the penpot/penpot GitHub
|
`scripts/gh.py` is a multi-purpose CLI for querying the penpot/penpot GitHub
|
||||||
repository via GraphQL and REST APIs through the authenticated `gh` CLI.
|
repository via GraphQL and REST APIs through the authenticated `gh` CLI.
|
||||||
|
|
||||||
## When to use
|
## When to use
|
||||||
@ -9,6 +9,7 @@ repository via GraphQL and REST APIs through the authenticated `gh` CLI.
|
|||||||
- Finding issues with no milestone.
|
- Finding issues with no milestone.
|
||||||
- Fetching PR details by number or by milestone.
|
- Fetching PR details by number or by milestone.
|
||||||
- Comparing milestone issues against CHANGES.md to find missing entries.
|
- Comparing milestone issues against CHANGES.md to find missing entries.
|
||||||
|
- Listing or inspecting GitHub Security Advisories (GHSA).
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@ -23,24 +24,24 @@ List issues in a milestone, with filtering by state, labels, and project status.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Closed issues in a milestone (default)
|
# Closed issues in a milestone (default)
|
||||||
python3 tools/gh.py issues "2.16.0"
|
python3 scripts/gh.py issues "2.16.0"
|
||||||
|
|
||||||
# All issues in a milestone
|
# All issues in a milestone
|
||||||
python3 tools/gh.py issues "2.16.0" --state all
|
python3 scripts/gh.py issues "2.16.0" --state all
|
||||||
|
|
||||||
# Issues with no milestone
|
# Issues with no milestone
|
||||||
python3 tools/gh.py issues none
|
python3 scripts/gh.py issues none
|
||||||
python3 tools/gh.py issues none --state open
|
python3 scripts/gh.py issues none --state open
|
||||||
|
|
||||||
# Filter by label (include only)
|
# Filter by label (include only)
|
||||||
python3 tools/gh.py issues "2.16.0" --label "bug"
|
python3 scripts/gh.py issues "2.16.0" --label "bug"
|
||||||
python3 tools/gh.py issues "2.16.0" --label "bug,regression"
|
python3 scripts/gh.py issues "2.16.0" --label "bug,regression"
|
||||||
|
|
||||||
# Exclude by label
|
# Exclude by label
|
||||||
python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog"
|
python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog"
|
||||||
|
|
||||||
# Show only issues NOT yet in CHANGES.md
|
# Show only issues NOT yet in CHANGES.md
|
||||||
python3 tools/gh.py issues "2.16.0" --compare CHANGES.md
|
python3 scripts/gh.py issues "2.16.0" --compare CHANGES.md
|
||||||
```
|
```
|
||||||
|
|
||||||
**Default filters** (override with flags):
|
**Default filters** (override with flags):
|
||||||
@ -55,23 +56,47 @@ Fetch PR details by number or by milestone.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Fetch specific PRs
|
# Fetch specific PRs
|
||||||
python3 tools/gh.py prs 9179 9204 9311
|
python3 scripts/gh.py prs 9179 9204 9311
|
||||||
|
|
||||||
# Read PR numbers from file
|
# Read PR numbers from file
|
||||||
python3 tools/gh.py prs --file prs.txt
|
python3 scripts/gh.py prs --file prs.txt
|
||||||
|
|
||||||
# Read PR numbers from stdin
|
# Read PR numbers from stdin
|
||||||
cat prs.txt | python3 tools/gh.py prs --stdin
|
cat prs.txt | python3 scripts/gh.py prs --stdin
|
||||||
|
|
||||||
# All PRs in a milestone (default: merged only)
|
# All PRs in a milestone (default: merged only)
|
||||||
python3 tools/gh.py prs --milestone "2.16.0"
|
python3 scripts/gh.py prs --milestone "2.16.0"
|
||||||
|
|
||||||
# All PRs in a milestone (all states)
|
# All PRs in a milestone (all states)
|
||||||
python3 tools/gh.py prs --milestone "2.16.0" --state all
|
python3 scripts/gh.py prs --milestone "2.16.0" --state all
|
||||||
```
|
```
|
||||||
|
|
||||||
**Output**: JSON array to stdout; progress to stderr.
|
**Output**: JSON array to stdout; progress to stderr.
|
||||||
|
|
||||||
|
### `advisories`
|
||||||
|
|
||||||
|
List or inspect GitHub Security Advisories for the repository.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all advisories (summary view)
|
||||||
|
python3 scripts/gh.py advisories
|
||||||
|
|
||||||
|
# Filter by severity
|
||||||
|
python3 scripts/gh.py advisories --severity critical
|
||||||
|
|
||||||
|
# Filter by state
|
||||||
|
python3 scripts/gh.py advisories --state triage
|
||||||
|
|
||||||
|
# Get full detail for a single advisory
|
||||||
|
python3 scripts/gh.py advisories GHSA-xvj6-fh9w-gjw7
|
||||||
|
```
|
||||||
|
|
||||||
|
**Summary output fields**: ghsa_id, cve_id, severity, cvss_score, state, summary, cwes, published_at, closed_at, url.
|
||||||
|
|
||||||
|
**Detail output** (single advisory) adds: description, vulnerabilities (package, version ranges), credits, timestamps.
|
||||||
|
|
||||||
|
**Output**: JSON to stdout; progress to stderr.
|
||||||
|
|
||||||
## Key principles
|
## Key principles
|
||||||
|
|
||||||
- All output is JSON — pipe into `jq` or other tools for further processing.
|
- All output is JSON — pipe into `jq` or other tools for further processing.
|
||||||
@ -1,7 +1,7 @@
|
|||||||
# nREPL Eval
|
# nREPL Eval
|
||||||
|
|
||||||
Evaluate Clojure (or ClojureScript) code via a running nREPL server using
|
Evaluate Clojure (or ClojureScript) code via a running nREPL server using
|
||||||
`tools/nrepl-eval.mjs` — a standalone CLI application.
|
`scripts/nrepl-eval.mjs` — a standalone CLI application.
|
||||||
|
|
||||||
Session state (defs, in-ns, etc.) persists across invocations via a stored
|
Session state (defs, in-ns, etc.) persists across invocations via a stored
|
||||||
session ID, so you can build up state incrementally.
|
session ID, so you can build up state incrementally.
|
||||||
@ -9,9 +9,9 @@ session ID, so you can build up state incrementally.
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node tools/nrepl-eval.mjs [options] [<code>]
|
node scripts/nrepl-eval.mjs [options] [<code>]
|
||||||
# or
|
# or
|
||||||
./tools/nrepl-eval.mjs [options] [<code>]
|
./scripts/nrepl-eval.mjs [options] [<code>]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
@ -47,37 +47,37 @@ Sessions are persisted to `/tmp/penpot-nrepl-session-<host>-<port>`. State
|
|||||||
carries across calls automatically:
|
carries across calls automatically:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs '(def x 42)'
|
./scripts/nrepl-eval.mjs '(def x 42)'
|
||||||
./tools/nrepl-eval.mjs 'x'
|
./scripts/nrepl-eval.mjs 'x'
|
||||||
# => 42
|
# => 42
|
||||||
```
|
```
|
||||||
|
|
||||||
Reset the session to start fresh:
|
Reset the session to start fresh:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs --reset-session '(def x 0)'
|
./scripts/nrepl-eval.mjs --reset-session '(def x 0)'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Evaluate code
|
### Evaluate code
|
||||||
|
|
||||||
**Single expression (inline) — uses default port 6064:**
|
**Single expression (inline) — uses default port 6064:**
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs '(+ 1 2 3)'
|
./scripts/nrepl-eval.mjs '(+ 1 2 3)'
|
||||||
```
|
```
|
||||||
|
|
||||||
**Backend nREPL (explicit):**
|
**Backend nREPL (explicit):**
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs --backend '(+ 1 2 3)'
|
./scripts/nrepl-eval.mjs --backend '(+ 1 2 3)'
|
||||||
```
|
```
|
||||||
|
|
||||||
**Frontend nREPL:**
|
**Frontend nREPL:**
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs --frontend '(js/alert "hi")'
|
./scripts/nrepl-eval.mjs --frontend '(js/alert "hi")'
|
||||||
```
|
```
|
||||||
|
|
||||||
**Multiple expressions via heredoc (recommended — avoids escaping issues):**
|
**Multiple expressions via heredoc (recommended — avoids escaping issues):**
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs <<'EOF'
|
./scripts/nrepl-eval.mjs <<'EOF'
|
||||||
(def x 10)
|
(def x 10)
|
||||||
(+ x 20)
|
(+ x 20)
|
||||||
EOF
|
EOF
|
||||||
@ -85,7 +85,7 @@ EOF
|
|||||||
|
|
||||||
**Override with a different port:**
|
**Override with a different port:**
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs -p 7888 '(+ 1 2 3)'
|
./scripts/nrepl-eval.mjs -p 7888 '(+ 1 2 3)'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Inspect last exception
|
### Inspect last exception
|
||||||
@ -93,26 +93,48 @@ EOF
|
|||||||
After code throws an error, retrieve the full exception details:
|
After code throws an error, retrieve the full exception details:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs -e
|
./scripts/nrepl-eval.mjs -e
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
**Require a namespace with reload:**
|
**Require a namespace with reload:**
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs "(require '[my.namespace :as ns] :reload)"
|
./scripts/nrepl-eval.mjs "(require '[my.namespace :as ns] :reload)"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test a function:**
|
**Test a function:**
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs "(ns/my-function arg1 arg2)"
|
./scripts/nrepl-eval.mjs "(ns/my-function arg1 arg2)"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Long-running operation with custom timeout:**
|
**Long-running operation with custom timeout:**
|
||||||
```bash
|
```bash
|
||||||
./tools/nrepl-eval.mjs -t 300000 "(long-running-fn)"
|
./scripts/nrepl-eval.mjs -t 300000 "(long-running-fn)"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Accessing Private Functions
|
||||||
|
|
||||||
|
Private functions (declared with `^:private` or `defn-`) cannot be called
|
||||||
|
directly from outside their namespace. Use the var quote syntax `#'` to
|
||||||
|
access the underlying var:
|
||||||
|
|
||||||
|
**This fails:**
|
||||||
|
```bash
|
||||||
|
./scripts/nrepl-eval.mjs "(app.rpc.commands.error-reports/build-list-query {})"
|
||||||
|
# => Syntax error: app.rpc.commands.error-reports/build-list-query is not public
|
||||||
|
```
|
||||||
|
|
||||||
|
**This works:**
|
||||||
|
```bash
|
||||||
|
./scripts/nrepl-eval.mjs "(#'app.rpc.commands.error-reports/build-list-query {})"
|
||||||
|
# => Returns the result
|
||||||
|
```
|
||||||
|
|
||||||
|
The `#'` reader macro resolves to `(var ...)`, giving you direct access to
|
||||||
|
the var regardless of its visibility modifier. The syntax is `#'` followed
|
||||||
|
by the fully qualified symbol.
|
||||||
|
|
||||||
## Key Principles
|
## Key Principles
|
||||||
|
|
||||||
- **Default port is 6064** — just pass code directly, no `-p` needed when
|
- **Default port is 6064** — just pass code directly, no `-p` needed when
|
||||||
43
.serena/memories/scripts/paren-repair.md
Normal file
43
.serena/memories/scripts/paren-repair.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Paren-Repair
|
||||||
|
|
||||||
|
`scripts/paren-repair` fixes mismatched parentheses, brackets, and braces in
|
||||||
|
Clojure/ClojureScript files, then reformats them with cljfmt.
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
- After LLM edits introduce broken delimiters — proactively run it on files
|
||||||
|
you just touched.
|
||||||
|
- When lint (clj-kondo), the Clojure compiler, or shadow-cljs report syntax
|
||||||
|
errors mentioning mismatched/unclosed delimiters, reader errors, or
|
||||||
|
unexpected EOF.
|
||||||
|
- Before running lint/format checks — delimiter errors make linter output
|
||||||
|
misleading. Fix them first, then lint.
|
||||||
|
|
||||||
|
## How to use (CLI)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# File mode (in-place fix + format)
|
||||||
|
bb scripts/paren-repair path/to/file.clj
|
||||||
|
|
||||||
|
# Pipe mode (stdin → fixed code to stdout)
|
||||||
|
echo '(def x 1' | bb scripts/paren-repair
|
||||||
|
|
||||||
|
# Help
|
||||||
|
bb scripts/paren-repair --help
|
||||||
|
```
|
||||||
|
`bb` must be invoked from the repo root so the path `scripts/paren-repair` resolves.
|
||||||
|
|
||||||
|
## Native Tool Available (opencode)
|
||||||
|
|
||||||
|
A native opencode tool `paren-repair` is available at `.opencode/scripts/paren-repair.ts`.
|
||||||
|
The LLM can call it directly with:
|
||||||
|
- `files`: Array of file paths to fix
|
||||||
|
- `code`: Code string to fix via stdin
|
||||||
|
|
||||||
|
Example usage by the LLM:
|
||||||
|
```
|
||||||
|
paren-repair(files="src/foo.clj, src/bar.cljs")
|
||||||
|
paren-repair(code="(defn foo [x")
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
39
.serena/memories/scripts/psql.md
Normal file
39
.serena/memories/scripts/psql.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Psql
|
||||||
|
|
||||||
|
`scripts/psql` is a wrapper around `psql` that connects to the Penpot PostgreSQL
|
||||||
|
database using environment variables (`PENPOT_DB_HOST`, `PENPOT_DB_USER`,
|
||||||
|
`PENPOT_DB_PASSWORD`, `PENPOT_DB_NAME`) with sensible defaults for local
|
||||||
|
development.
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
- Running ad-hoc SQL queries against the Penpot database.
|
||||||
|
- Inspecting schema, migrations, or data during development or debugging.
|
||||||
|
|
||||||
|
## How to use (CLI)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default connection (penpot db, localhost)
|
||||||
|
scripts/psql -c "SELECT version();"
|
||||||
|
|
||||||
|
# Test database
|
||||||
|
scripts/psql --test -c "SELECT * FROM migrations;"
|
||||||
|
|
||||||
|
# Custom host/user/database
|
||||||
|
scripts/psql --host myhost --user myuser --db mydb
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/psql` must be invoked from the repo root so the path resolves.
|
||||||
|
|
||||||
|
## Native Tool Available (opencode)
|
||||||
|
|
||||||
|
A native opencode tool `penpot-psql` is available. The LLM can call it directly
|
||||||
|
with:
|
||||||
|
- `sql`: SQL command string to execute
|
||||||
|
- `test`: Boolean flag to use the `penpot_test` database
|
||||||
|
|
||||||
|
Example usage by the LLM:
|
||||||
|
```
|
||||||
|
penpot-psql(sql="SELECT version();")
|
||||||
|
penpot-psql(sql="SELECT * FROM migrations;", test=true)
|
||||||
|
```
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# Taiga API client
|
# Taiga API client
|
||||||
|
|
||||||
`tools/taiga.py` fetches public issues, user stories, and tasks from the
|
`scripts/taiga.py` fetches public issues, user stories, and tasks from the
|
||||||
Penpot Taiga project (id 345963) without authentication.
|
Penpot Taiga project (id 345963) without authentication.
|
||||||
|
|
||||||
## When to use
|
## When to use
|
||||||
@ -13,16 +13,16 @@ Penpot Taiga project (id 345963) without authentication.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Fetch by full Taiga URL
|
# Fetch by full Taiga URL
|
||||||
python3 tools/taiga.py https://tree.taiga.io/project/penpot/issue/13714
|
python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714
|
||||||
|
|
||||||
# Fetch by type and ref number
|
# Fetch by type and ref number
|
||||||
python3 tools/taiga.py issue 13714
|
python3 scripts/taiga.py issue 13714
|
||||||
python3 tools/taiga.py us 14128
|
python3 scripts/taiga.py us 14128
|
||||||
python3 tools/taiga.py task 13648
|
python3 scripts/taiga.py task 13648
|
||||||
|
|
||||||
# Output raw JSON instead of formatted summary
|
# Output raw JSON instead of formatted summary
|
||||||
python3 tools/taiga.py --json issue 13714
|
python3 scripts/taiga.py --json issue 13714
|
||||||
python3 tools/taiga.py --json https://tree.taiga.io/project/penpot/us/14128
|
python3 scripts/taiga.py --json https://tree.taiga.io/project/penpot/us/14128
|
||||||
```
|
```
|
||||||
|
|
||||||
## Supported types
|
## Supported types
|
||||||
@ -137,17 +137,32 @@ E2E tests should not be added unless explicitly requested.
|
|||||||
|
|
||||||
## Execution discipline
|
## Execution discipline
|
||||||
|
|
||||||
When running CLJS/JS tests (frontend, common):
|
**CRITICAL: Test output handling rules**
|
||||||
|
|
||||||
|
When running ANY test command (CLJS/JS or JVM):
|
||||||
|
|
||||||
|
1. **NEVER pipe test output directly to `| head`, `| tail`, `| grep`, or similar filters** — this can hide failures and cause you to miss critical errors.
|
||||||
|
2. **ALWAYS pipe to a file first, then read the file:**
|
||||||
|
```bash
|
||||||
|
# CORRECT:
|
||||||
|
pnpm run test 2>&1 > /tmp/test-output.txt
|
||||||
|
grep -A 5 "failures" /tmp/test-output.txt
|
||||||
|
|
||||||
|
# WRONG:
|
||||||
|
pnpm run test 2>&1 | tail -20
|
||||||
|
pnpm run test 2>&1 | grep "failures"
|
||||||
|
```
|
||||||
|
3. **Use `--focus` to narrow test scope** instead of filtering output.
|
||||||
|
4. **Read the full output file** to understand test results completely.
|
||||||
|
|
||||||
|
When running CLJS/JS tests (frontend, common):
|
||||||
- **Always use `pnpm run test:quiet`** — it silently builds the test bundle then runs the test runner, giving you clean test output.
|
- **Always use `pnpm run test:quiet`** — it silently builds the test bundle then runs the test runner, giving you clean test output.
|
||||||
- **Never pipe test output through `tail`, `head`, or similar filters** — doing so can silently hide test failures. Use `--focus` to narrow scope instead.
|
|
||||||
- **If you need to filter output, tee to a temp file first:** `pnpm run test:quiet 2>&1 | tee /tmp/penpot-test-output.txt`. The full output is preserved on disk so you can `grep`/`tail`/`head` the file without re-running.
|
|
||||||
- Use `pnpm run test` when you want to see build output alongside test results (always builds, then runs).
|
- Use `pnpm run test` when you want to see build output alongside test results (always builds, then runs).
|
||||||
- After `build:test` has been run once, you can invoke the runner directly: `node target/tests/test.js [--focus ...] [--log-level ...]`.
|
- After `build:test` has been run once, you can invoke the runner directly: `node target/tests/test.js [--focus ...] [--log-level ...]`.
|
||||||
|
|
||||||
When running JVM tests (backend, common):
|
When running JVM tests (backend, common):
|
||||||
- Use `clojure -M:dev:test` directly (no pnpm wrapper).
|
- Use `clojure -M:dev:test` directly (no pnpm wrapper).
|
||||||
- The same no-piping rule applies: use `--focus` to narrow scope.
|
- Same file-piping rule applies.
|
||||||
|
|
||||||
## Verification Checklist
|
## Verification Checklist
|
||||||
|
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
# Paren-Repair
|
|
||||||
|
|
||||||
`tools/paren-repair.bb` fixes mismatched parentheses, brackets, and braces in
|
|
||||||
Clojure/ClojureScript files, then reformats them with cljfmt.
|
|
||||||
|
|
||||||
## When to use
|
|
||||||
|
|
||||||
- After LLM edits introduce broken delimiters — proactively run it on files
|
|
||||||
you just touched.
|
|
||||||
- When lint (clj-kondo), the Clojure compiler, or shadow-cljs report syntax
|
|
||||||
errors mentioning mismatched/unclosed delimiters, reader errors, or
|
|
||||||
unexpected EOF.
|
|
||||||
- Before running lint/format checks — delimiter errors make linter output
|
|
||||||
misleading. Fix them first, then lint.
|
|
||||||
|
|
||||||
## How to use
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# File mode (in-place fix + format)
|
|
||||||
bb tools/paren-repair.bb path/to/file.clj
|
|
||||||
|
|
||||||
# Pipe mode (stdin → fixed code to stdout)
|
|
||||||
echo '(def x 1' | bb tools/paren-repair.bb
|
|
||||||
|
|
||||||
# Help
|
|
||||||
bb tools/paren-repair.bb --help
|
|
||||||
```
|
|
||||||
|
|
||||||
`bb` must be invoked from the repo root so the path `tools/paren-repair.bb` resolves.
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
# PostgreSQL client wrapper
|
|
||||||
|
|
||||||
`tools/psql` is a wrapper around the `psql` command with defaults preconfigured
|
|
||||||
for the Penpot development environment.
|
|
||||||
|
|
||||||
## When to use
|
|
||||||
|
|
||||||
- Running SQL queries against the dev database (`penpot`) or test database
|
|
||||||
(`penpot_test`).
|
|
||||||
- Inspecting table structures, running migrations manually, or debugging
|
|
||||||
database state.
|
|
||||||
- Any time you need PostgreSQL access and want the correct host/user/password
|
|
||||||
without typing them each time.
|
|
||||||
|
|
||||||
## How to use
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Interactive session (penpot database)
|
|
||||||
tools/psql
|
|
||||||
|
|
||||||
# Interactive session (penpot_test database)
|
|
||||||
tools/psql --test
|
|
||||||
|
|
||||||
# Inline query (penpot)
|
|
||||||
tools/psql -c "SELECT 1"
|
|
||||||
|
|
||||||
# Inline query (penpot_test)
|
|
||||||
tools/psql --test -c "SELECT 1"
|
|
||||||
|
|
||||||
# Override defaults
|
|
||||||
tools/psql -h other-host -U other-user -d other-db
|
|
||||||
|
|
||||||
# Pipe SQL from a file
|
|
||||||
tools/psql -f some-query.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
All standard `psql` flags are passed through after the wrapper's own flags.
|
|
||||||
|
|
||||||
## Defaults
|
|
||||||
|
|
||||||
| Setting | Default | Env override |
|
|
||||||
|----------|-----------|---------------------|
|
|
||||||
| Host | `postgres` | `PENPOT_DB_HOST` |
|
|
||||||
| User | `penpot` | `PENPOT_DB_USER` |
|
|
||||||
| Password | `penpot` | `PENPOT_DB_PASSWORD` |
|
|
||||||
| Database | `penpot` | `PENPOT_DB_NAME` |
|
|
||||||
|
|
||||||
## See also
|
|
||||||
|
|
||||||
`tools/db-schema` — a companion script that dumps the current DDL schema
|
|
||||||
using `pg_dump --schema-only`, with the same defaults and `--test` flag.
|
|
||||||
@ -14,6 +14,8 @@ automatically pull the identity from the local git config `user.name` and `user.
|
|||||||
:emoji: Subject line (imperative, capitalized, no period, <=70 chars)
|
:emoji: Subject line (imperative, capitalized, no period, <=70 chars)
|
||||||
|
|
||||||
Body explaining what changed and why.
|
Body explaining what changed and why.
|
||||||
|
Wrap lines at 72 characters — git log and tooling
|
||||||
|
render long lines poorly. Keep each line concise.
|
||||||
|
|
||||||
AI-assisted-by: model-name
|
AI-assisted-by: model-name
|
||||||
```
|
```
|
||||||
@ -25,3 +27,7 @@ AI-assisted-by: model-name
|
|||||||
## Commit Type Emojis
|
## Commit Type Emojis
|
||||||
|
|
||||||
`:bug:` bug fix · `:sparkles:` enhancement · `:tada:` new feature · `:recycle:` refactor · `:lipstick:` cosmetic · `:ambulance:` critical fix · `:books:` docs · `:construction:` WIP · `:boom:` breaking · `:wrench:` config · `:zap:` perf · `:whale:` docker · `:paperclip:` other · `:arrow_up:` dep upgrade · `:arrow_down:` dep downgrade · `:fire:` removal · `:globe_with_meridians:` translations · `:rocket:` epic/highlight
|
`:bug:` bug fix · `:sparkles:` enhancement · `:tada:` new feature · `:recycle:` refactor · `:lipstick:` cosmetic · `:ambulance:` critical fix · `:books:` docs · `:construction:` WIP · `:boom:` breaking · `:wrench:` config · `:zap:` perf · `:whale:` docker · `:paperclip:` other · `:arrow_up:` dep upgrade · `:arrow_down:` dep downgrade · `:fire:` removal · `:globe_with_meridians:` translations · `:rocket:` epic/highlight
|
||||||
|
|
||||||
|
## Referencing Issues
|
||||||
|
|
||||||
|
Use `Closes #NNNN` (not `Fixes #NNNN`) to link a commit to a GitHub issue.
|
||||||
|
|||||||
@ -4,10 +4,10 @@ PR only on explicit request. Branch: issue/feature-specific; fallback `<type>/<s
|
|||||||
|
|
||||||
## Target Branch
|
## Target Branch
|
||||||
|
|
||||||
Auto-detect the base branch with `tools/detect-target-branch`:
|
Auto-detect the base branch with `scripts/detect-target-branch`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
TARGET=$(tools/detect-target-branch)
|
TARGET=$(scripts/detect-target-branch)
|
||||||
```
|
```
|
||||||
|
|
||||||
This outputs `staging` or `develop` by walking the local commit graph (pure local, no remote/network). Do not ask the user for the target branch unless the tool fails.
|
This outputs `staging` or `develop` by walking the local commit graph (pure local, no remote/network). Do not ask the user for the target branch unless the tool fails.
|
||||||
@ -30,7 +30,7 @@ See `mem:workflow/creating-commits` for emoji codes. Squash merge uses the PR ti
|
|||||||
|
|
||||||
Include concise sections covering:
|
Include concise sections covering:
|
||||||
- what changed and why;
|
- what changed and why;
|
||||||
- related GitHub issues or Taiga stories (`Fixes #NNNN`, `Relates to #NNNN`, `Taiga #NNNN`);
|
- related GitHub issues or Taiga stories (`Closes #NNNN`, `Relates to #NNNN`, `Taiga #NNNN`);
|
||||||
- screenshots or recordings for UI-visible changes;
|
- screenshots or recordings for UI-visible changes;
|
||||||
- testing performed and residual risk;
|
- testing performed and residual risk;
|
||||||
- breaking changes or migration notes, if any.
|
- breaking changes or migration notes, if any.
|
||||||
@ -42,15 +42,15 @@ PR descriptions follow this structure:
|
|||||||
|
|
||||||
## What
|
## What
|
||||||
|
|
||||||
<one paragraph: the problem or feature, user-facing impact>
|
<the problem or feature and its user-facing impact — short bullet items where there is more than one point>
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
<root cause or motivation, why this change was necessary>
|
<root cause or motivation — a short paragraph or bullets>
|
||||||
|
|
||||||
## How
|
## How
|
||||||
|
|
||||||
<high-level approach, key technical decisions>
|
<high-level approach and key decisions — bullet items, grouped by area (bold lead-ins) for larger PRs>
|
||||||
```
|
```
|
||||||
|
|
||||||
The "Note:" line is required at the top. Adjust if this is a manual (non-AI) PR.
|
The "Note:" line is required at the top. Adjust if this is a manual (non-AI) PR.
|
||||||
@ -59,6 +59,8 @@ The "Note:" line is required at the top. Adjust if this is a manual (non-AI) PR.
|
|||||||
|
|
||||||
- **Write for humans.** The diff shows what changed. The description explains why.
|
- **Write for humans.** The diff shows what changed. The description explains why.
|
||||||
- **Be concise.** Focus on reasoning: What was the problem? Why did it happen? How did you solve it?
|
- **Be concise.** Focus on reasoning: What was the problem? Why did it happen? How did you solve it?
|
||||||
|
- **Prefer bullets over paragraphs.** Short bullet items, grouped by area with bold lead-ins where helpful, are far easier to digest than prose; keep any remaining paragraph to a few sentences.
|
||||||
|
- **No manual line wraps.** Markdown renders adapting to the viewport; hard-wrapped lines degrade rendering. One line per paragraph or bullet, however long.
|
||||||
- **Skip the obvious.** Don't explain what `git diff` already shows.
|
- **Skip the obvious.** Don't explain what `git diff` already shows.
|
||||||
|
|
||||||
### What NOT to Include
|
### What NOT to Include
|
||||||
@ -83,7 +85,7 @@ cat > /tmp/pr-body.md << 'PR_BODY'
|
|||||||
<body content here>
|
<body content here>
|
||||||
PR_BODY
|
PR_BODY
|
||||||
|
|
||||||
TARGET=$(tools/detect-target-branch)
|
TARGET=$(scripts/detect-target-branch)
|
||||||
|
|
||||||
gh pr create \
|
gh pr create \
|
||||||
--repo penpot/penpot \
|
--repo penpot/penpot \
|
||||||
|
|||||||
@ -1,26 +1,31 @@
|
|||||||
# the name by which the project can be referenced within Serena
|
# the name by which the project can be referenced within Serena/when chatting with the LLM.
|
||||||
project_name: "penpot"
|
project_name: "penpot"
|
||||||
|
|
||||||
|
# list of languages for which language servers are started (LSP backend only); choose from:
|
||||||
# list of languages for which language servers are started; choose from:
|
# ada al angular ansible bash
|
||||||
# al ansible bash clojure cpp
|
# bsl clojure cpp cpp_ccls crystal
|
||||||
# cpp_ccls crystal csharp csharp_omnisharp dart
|
# csharp csharp_omnisharp cue dart elixir
|
||||||
# elixir elm erlang fortran fsharp
|
# elm erlang fortran fsharp gdscript
|
||||||
# go groovy haskell haxe hlsl
|
# go groovy haskell haxe hlsl
|
||||||
# java json julia kotlin lean4
|
# html java json julia kotlin
|
||||||
# lua luau markdown matlab msl
|
# latex lean4 lua luau markdown
|
||||||
# nix ocaml pascal perl php
|
# matlab msl nix ocaml pascal
|
||||||
# php_phpactor powershell python python_jedi python_ty
|
# perl php php_phpactor php_phpantom powershell
|
||||||
# r rego ruby ruby_solargraph rust
|
# python python_jedi python_pyrefly python_ty r
|
||||||
# scala solidity swift systemverilog terraform
|
# rego ruby ruby_solargraph rust scala
|
||||||
# toml typescript typescript_vts vue yaml
|
# scss solidity svelte swift systemverilog
|
||||||
# zig
|
# terraform toml typescript typescript_vts vue
|
||||||
# (This list may be outdated. For the current list, see values of Language enum here:
|
# yaml zig
|
||||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
# (This list may be outdated; generated with scripts/print_language_list.py;
|
||||||
|
# For the current list, see values of Language enum here:
|
||||||
|
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
|
||||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||||
# Note:
|
# Note:
|
||||||
# - For C, use cpp
|
# - For C, use cpp
|
||||||
# - For JavaScript, use typescript
|
# - For JavaScript, use typescript
|
||||||
|
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
|
||||||
|
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
|
||||||
|
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
|
||||||
# - For Free Pascal/Lazarus, use pascal
|
# - For Free Pascal/Lazarus, use pascal
|
||||||
# Special requirements:
|
# Special requirements:
|
||||||
# Some languages require additional setup/installations.
|
# Some languages require additional setup/installations.
|
||||||
@ -54,12 +59,19 @@ ignore_all_files_in_gitignore: true
|
|||||||
|
|
||||||
# advanced configuration option allowing to configure language server-specific options.
|
# advanced configuration option allowing to configure language server-specific options.
|
||||||
# Maps the language key to the options.
|
# Maps the language key to the options.
|
||||||
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
|
# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
|
||||||
# No documentation on options means no options are available.
|
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
|
||||||
ls_specific_settings: {}
|
ls_specific_settings: {}
|
||||||
|
|
||||||
# list of additional paths to ignore in this project.
|
# list of additional paths to ignore in this project.
|
||||||
# Same syntax as gitignore, so you can use * and **.
|
# Same syntax as gitignore, so you can use * and **.
|
||||||
|
# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases.
|
||||||
|
# Example:
|
||||||
|
# ignored_paths:
|
||||||
|
# - "examples/**"
|
||||||
|
# - ".worktrees/**"
|
||||||
|
# - "**/bin/**"
|
||||||
|
# - "**/obj/**"
|
||||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||||
ignored_paths: []
|
ignored_paths: []
|
||||||
|
|
||||||
@ -130,13 +142,38 @@ ignored_memory_patterns: []
|
|||||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||||
added_modes:
|
added_modes:
|
||||||
|
|
||||||
# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos).
|
# list of additional workspace folder paths for cross-package reference support.
|
||||||
# Paths can be absolute or relative to the project root.
|
# Paths can be absolute or relative to the project root.
|
||||||
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
|
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
|
||||||
# symbols and references across package boundaries.
|
# symbols and references across package boundaries, but these folders are not indexed by Serena,
|
||||||
# Currently supported for: TypeScript.
|
# i.e. the respective symbols will not be found using Serena's symbol search tools.
|
||||||
# Example:
|
# Example:
|
||||||
# additional_workspace_folders:
|
# additional_workspace_folders:
|
||||||
# - ../sibling-package
|
# - ../sibling-package
|
||||||
# - ../shared-lib
|
# - ../shared-lib
|
||||||
additional_workspace_folders: []
|
ls_additional_workspace_folders: []
|
||||||
|
|
||||||
|
# list of workspace folder paths (LSP backend only).
|
||||||
|
# These folders will be used to build up Serena's symbol index.
|
||||||
|
# Paths must be within the project root and should thus be relative to the project root.
|
||||||
|
# Furthermore, the paths should not be filtered by ignore settings.
|
||||||
|
# Default setting: The entire project root folder (".") is considered.
|
||||||
|
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
|
||||||
|
# ls_workspace_folders:
|
||||||
|
# - "./subproject1"
|
||||||
|
# - "./subproject2"
|
||||||
|
ls_workspace_folders:
|
||||||
|
- .
|
||||||
|
|
||||||
|
# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
|
||||||
|
# the command runs in the project root directory and is only executed if the project is trusted
|
||||||
|
# (see trusted_project_path_patterns in the global configuration).
|
||||||
|
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
|
||||||
|
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
|
||||||
|
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
|
||||||
|
# example: activation_command: "npx nx run-many -t build"
|
||||||
|
activation_command:
|
||||||
|
|
||||||
|
# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
|
||||||
|
# must be a positive number.
|
||||||
|
activation_command_timeout: 180.0
|
||||||
|
|||||||
38
AGENTS.md
38
AGENTS.md
@ -1,6 +1,6 @@
|
|||||||
# AI AGENT GUIDE
|
# AI AGENT GUIDE
|
||||||
|
|
||||||
## Hard rules (always apply — no exceptions)
|
## HARD RULES (always apply — no exceptions)
|
||||||
|
|
||||||
- **Never `git push`, force-push, or modify `git origin`** (or any other remote).
|
- **Never `git push`, force-push, or modify `git origin`** (or any other remote).
|
||||||
The user pushes from their own shell. If a push is required to surface the
|
The user pushes from their own shell. If a push is required to surface the
|
||||||
@ -8,6 +8,9 @@
|
|||||||
wait for the user to push. Do not change the remote URL, do not switch SSH↔HTTPS.
|
wait for the user to push. Do not change the remote URL, do not switch SSH↔HTTPS.
|
||||||
- **Never amend a commit that has been pushed** unless the user explicitly asks.
|
- **Never amend a commit that has been pushed** unless the user explicitly asks.
|
||||||
If the user pushes, treat that commit as final from the agent's side.
|
If the user pushes, treat that commit as final from the agent's side.
|
||||||
|
- **Never pipe test output directly to filters** (`| head`, `| tail`, `| grep`, etc.).
|
||||||
|
Always redirect to a file first: `command > /tmp/output.txt 2>&1`, then read/grep the file.
|
||||||
|
This prevents hiding test failures. See `mem:testing` for details.
|
||||||
- **Read the workflow memory BEFORE the corresponding action**:
|
- **Read the workflow memory BEFORE the corresponding action**:
|
||||||
- Before `git commit` → `mem:workflow/creating-commits` (commit format, AI-assisted-by trailer)
|
- Before `git commit` → `mem:workflow/creating-commits` (commit format, AI-assisted-by trailer)
|
||||||
- Before `gh issue create` → `mem:workflow/creating-issues` (title derivation, body template, Issue Type)
|
- Before `gh issue create` → `mem:workflow/creating-issues` (title derivation, body template, Issue Type)
|
||||||
@ -31,6 +34,19 @@ Skipping this step is the #1 cause of incorrect or incomplete work.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Auto-triggers
|
||||||
|
|
||||||
|
- **Security advisory URL pasted** — When the user pastes a URL matching
|
||||||
|
`github.com/penpot/penpot/security/advisories/GHSA-*`, extract the GHSA ID
|
||||||
|
from the URL and run `python3 scripts/gh.py advisories <GHSA-ID>` to fetch
|
||||||
|
full advisory details before proceeding.
|
||||||
|
|
||||||
|
## Writing Rules
|
||||||
|
|
||||||
|
Use the `ste` skill when the user explicitly requests STE, `/ste`, or ASD-STE100.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Memory system
|
# Memory system
|
||||||
|
|
||||||
Memories are the **primary project guidance** — not docs or readme files.
|
Memories are the **primary project guidance** — not docs or readme files.
|
||||||
@ -92,3 +108,23 @@ precision while maintaining a strong focus on maintainability and performance.
|
|||||||
down into atomic steps.
|
down into atomic steps.
|
||||||
2. Be concise and autonomous.
|
2. Be concise and autonomous.
|
||||||
3. Do **not** touch unrelated modules unless the task explicitly requires it.
|
3. Do **not** touch unrelated modules unless the task explicitly requires it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Available Scripts & Tools
|
||||||
|
|
||||||
|
## Native opencode Tools (callable directly by the LLM)
|
||||||
|
|
||||||
|
- `paren-repair` — Fix mismatched delimiters + reformat Clojure files. Example: `paren-repair(files="src/foo.clj, src/bar.cljs")`
|
||||||
|
- `penpot-psql` — Execute SQL against the Penpot database. Example: `penpot-psql(sql="SELECT version();")`
|
||||||
|
|
||||||
|
## Scripts (from repo root via `scripts/<name>`)
|
||||||
|
|
||||||
|
- `scripts/paren-repair` — Fix mismatched delimiters in Clojure/CLJS files + reformat with cljfmt. See `mem:scripts/paren-repair`.
|
||||||
|
- `scripts/psql` — Connect to the Penpot PostgreSQL database (wraps `psql` with env-var defaults). See `mem:scripts/psql`.
|
||||||
|
- `scripts/nrepl-eval.mjs` — Evaluate Clojure code via nREPL (backend + frontend).
|
||||||
|
- `scripts/check-commit` — Validate commit messages against Penpot's commit guidelines.
|
||||||
|
- `scripts/check-fmt-clj` — Check Clojure formatting without modifying files.
|
||||||
|
- `scripts/ci` — CI orchestration script for running lint, tests, and format checks across modules. See `scripts/ci --help`.
|
||||||
|
- `scripts/gh.py` — Multi-purpose GitHub CLI helper. Subcommands: `issues` (list issues in a milestone), `prs` (fetch PR details), `advisories` (list/inspect security advisories). See `python3 scripts/gh.py --help`.
|
||||||
|
|
||||||
|
|||||||
104
CHANGES.md
104
CHANGES.md
@ -1,8 +1,56 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## 2.17.0 (Unreleased)
|
## 2.18.0 (Unreleased)
|
||||||
|
|
||||||
### :boom: Breaking changes & Deprecations
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
|
- Fix MCP integration hanging when the Penpot tab is backgrounded or frozen by the browser [#10323](https://github.com/penpot/penpot/issues/10323) (PR: [#10392](https://github.com/penpot/penpot/pull/10392))
|
||||||
|
- Fix synced component copy not reflowing children after spacing token update [#9892](https://github.com/penpot/penpot/issues/9892)
|
||||||
|
- Fix spacebar activating pan mode while typing a comment (by @Krishcode264) [#10285](https://github.com/penpot/penpot/issues/10285) (PR: [#10287](https://github.com/penpot/penpot/pull/10287))
|
||||||
|
- Fix plugin API rejecting negative letterSpacing values (by @filipsajdak) [#9780](https://github.com/penpot/penpot/issues/9780) (PR: [#10257](https://github.com/penpot/penpot/pull/10257))
|
||||||
|
- Fix plugin API addTheme calls failing with the signature shown in the high-level overview [#10074](https://github.com/penpot/penpot/issues/10074) (PR: [#10359](https://github.com/penpot/penpot/pull/10359))
|
||||||
|
- Fix empty text shape not being deleted on editor exit [#10540](https://github.com/penpot/penpot/issues/10540) (PR: [#10541](https://github.com/penpot/penpot/pull/10541))
|
||||||
|
- Fix broken token pills showing wrong default state when not selected [#10524](https://github.com/penpot/penpot/issues/10524) (PR: [#10535](https://github.com/penpot/penpot/pull/10535))
|
||||||
|
- Replace hyphens with bullets in subscription benefits list [#10547](https://github.com/penpot/penpot/issues/10547) (PR: [#10523](https://github.com/penpot/penpot/pull/10523))
|
||||||
|
- Fix Chinese (zh-CN) translation showing wrong label for Intersection in board path menu (by @sawirricardo) [#10346](https://github.com/penpot/penpot/issues/10346) (PR: [#10381](https://github.com/penpot/penpot/pull/10381))
|
||||||
|
|
||||||
|
### :sparkles: New features & Enhancements
|
||||||
|
|
||||||
|
- Group toolbar drawing tools into shape and free-draw flyouts [#9316](https://github.com/penpot/penpot/issues/9316) (PR: [#9480](https://github.com/penpot/penpot/pull/9480), [#10354](https://github.com/penpot/penpot/pull/10354))
|
||||||
|
- Add outline stroke to Paths [#9961](https://github.com/penpot/penpot/issues/9961) (PR: [#8677](https://github.com/penpot/penpot/pull/8677))
|
||||||
|
- Make throwValidationErrors default to true for v2 manifest plugins [#10401](https://github.com/penpot/penpot/issues/10401) (PR: [#10433](https://github.com/penpot/penpot/pull/10433))
|
||||||
|
- Add dedicated Line and Arrow drawing tools (by @davidv399) [#9145](https://github.com/penpot/penpot/issues/9145) (PR: [#9146](https://github.com/penpot/penpot/pull/9146))
|
||||||
|
- Refactor wasm rulers and UI state [#10116](https://github.com/penpot/penpot/issues/10116) (PR: [#10461](https://github.com/penpot/penpot/pull/10461))
|
||||||
|
- Improve team invitations modal in the dashboard [#10484](https://github.com/penpot/penpot/issues/10484) (PR: [#10459](https://github.com/penpot/penpot/pull/10459))
|
||||||
|
|
||||||
|
|
||||||
|
## 2.17.1
|
||||||
|
|
||||||
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
|
- Fix overrides lost after switching component variant [#10588](https://github.com/penpot/penpot/issues/10588) (PR: [#10619](https://github.com/penpot/penpot/pull/10619))
|
||||||
|
- Fix malformed get-font-variants request when team-id is missing from dashboard URL [#10644](https://github.com/penpot/penpot/issues/10644) (PR: [#10645](https://github.com/penpot/penpot/pull/10645))
|
||||||
|
- Fix malformed get-profiles-for-file-comments request when file-id is missing from workspace URL [#10652](https://github.com/penpot/penpot/issues/10652) (PR: [#10655](https://github.com/penpot/penpot/pull/10655))
|
||||||
|
- Fix internal error when dragging inner layout with Boolean operations [#10647](https://github.com/penpot/penpot/issues/10647) (PR: [#10778](https://github.com/penpot/penpot/pull/10778))
|
||||||
|
- Fix frontend throwing raw TypeError on undefined .getData receivers across import, paste, drag, and text editor paths [#10709](https://github.com/penpot/penpot/issues/10709) (PR: [#10718](https://github.com/penpot/penpot/pull/10718))
|
||||||
|
- Fix workspace crash with 'can't access dead object' in Firefox when navigating between pages [#10719](https://github.com/penpot/penpot/issues/10719) (PR: [#10721](https://github.com/penpot/penpot/pull/10721))
|
||||||
|
- Fix workspace crash when holding an arrow key on a selection due to excessive re-renders [#10726](https://github.com/penpot/penpot/issues/10726) (PR: [#10736](https://github.com/penpot/penpot/pull/10736))
|
||||||
|
- Fix dashboard sidebar throwing removeChild NotFoundError during rapid keyboard navigation [#10714](https://github.com/penpot/penpot/issues/10714) (PR: [#10715](https://github.com/penpot/penpot/pull/10715))
|
||||||
|
- Fix asset download failing with S3 auth conflict when using access token [#10776](https://github.com/penpot/penpot/issues/10776) (PR: [#10777](https://github.com/penpot/penpot/pull/10777))
|
||||||
|
- Fix import worker crashing when importing non-Penpot zip files [#10781](https://github.com/penpot/penpot/issues/10781) (PR: [#10782](https://github.com/penpot/penpot/pull/10782))
|
||||||
|
- Fix viewer crash with WASM panic when opening URL with page-id [#10800](https://github.com/penpot/penpot/issues/10800) (PR: [#10805](https://github.com/penpot/penpot/pull/10805))
|
||||||
|
- Fix backend returning 500 when JSON request body has unrecognized escape sequence [#10804](https://github.com/penpot/penpot/issues/10804) (PR: [#10808](https://github.com/penpot/penpot/pull/10808))
|
||||||
|
- Fix color picker eyedropper crashing when viewport is unmounted during pointer move [#10811](https://github.com/penpot/penpot/issues/10811) (PR: [#10812](https://github.com/penpot/penpot/pull/10812))
|
||||||
|
- Fix flex layout crash when dragging shapes with missing bounds [#10843](https://github.com/penpot/penpot/issues/10843) (PR: [#10845](https://github.com/penpot/penpot/pull/10845))
|
||||||
|
- Fix export failing when shape has blank layer name [#10849](https://github.com/penpot/penpot/issues/10849) (PR: [#10852](https://github.com/penpot/penpot/pull/10852))
|
||||||
|
- Fix area selection (marquee) being aborted by select-shapes interrupt [#10872](https://github.com/penpot/penpot/issues/10872) (PR: [#10870](https://github.com/penpot/penpot/pull/10870))
|
||||||
|
- Fix gradient editor sending invalid stop offset when clicking outside gradient line [#10879](https://github.com/penpot/penpot/issues/10879) (PR: [#10881](https://github.com/penpot/penpot/pull/10881))
|
||||||
|
- Fix audit event validation failing when error reports contain string profile-id and missing token context [#10897](https://github.com/penpot/penpot/issues/10897) (PR: [#10898](https://github.com/penpot/penpot/pull/10898))
|
||||||
|
- Fix MCP tool call timeout being too low for some operations [#10953](https://github.com/penpot/penpot/issues/10953) (PR: [#10967](https://github.com/penpot/penpot/pull/10967))
|
||||||
|
- Fix MCP requests running into timeouts after leaving a file in Penpot [#10958](https://github.com/penpot/penpot/issues/10958) (PR: [#10967](https://github.com/penpot/penpot/pull/10967))
|
||||||
|
- Fix duplicate WebSocket MCP connection attempts deregistering the original connection's routing entries [#10961](https://github.com/penpot/penpot/issues/10961) (PR: [#10967](https://github.com/penpot/penpot/pull/10967))
|
||||||
|
|
||||||
|
## 2.17.0
|
||||||
|
|
||||||
### :rocket: Epics and highlights
|
### :rocket: Epics and highlights
|
||||||
|
|
||||||
@ -22,7 +70,7 @@
|
|||||||
- Remove unreachable try/catch in hex->hsl (by @Dexterity104) [#9244](https://github.com/penpot/penpot/issues/9244) (PR: [#9245](https://github.com/penpot/penpot/pull/9245))
|
- Remove unreachable try/catch in hex->hsl (by @Dexterity104) [#9244](https://github.com/penpot/penpot/issues/9244) (PR: [#9245](https://github.com/penpot/penpot/pull/9245))
|
||||||
- Remove stray debug log in exporter upload-resource (by @iot2edge) [#9270](https://github.com/penpot/penpot/issues/9270) (PR: [#9272](https://github.com/penpot/penpot/pull/9272))
|
- Remove stray debug log in exporter upload-resource (by @iot2edge) [#9270](https://github.com/penpot/penpot/issues/9270) (PR: [#9272](https://github.com/penpot/penpot/pull/9272))
|
||||||
- Release pool connection during font variant creation (by @Dexterity104) [#9286](https://github.com/penpot/penpot/issues/9286) (PR: [#9287](https://github.com/penpot/penpot/pull/9287))
|
- Release pool connection during font variant creation (by @Dexterity104) [#9286](https://github.com/penpot/penpot/issues/9286) (PR: [#9287](https://github.com/penpot/penpot/pull/9287))
|
||||||
- Add autocomplete combobox to token creation and edition forms [#9899](https://github.com/penpot/penpot/issues/9899) (PR: [#9109](https://github.com/penpot/penpot/pull/9109))
|
- Add autocomplete combobox to token creation and edition forms [#9899](https://github.com/penpot/penpot/issues/9899) (PR: [#9109](https://github.com/penpot/penpot/pull/9109), [#8294](https://github.com/penpot/penpot/pull/8294))
|
||||||
- Add list view mode to color picker UI [#4420](https://github.com/penpot/penpot/issues/4420) (PR: [#9953](https://github.com/penpot/penpot/pull/9953))
|
- Add list view mode to color picker UI [#4420](https://github.com/penpot/penpot/issues/4420) (PR: [#9953](https://github.com/penpot/penpot/pull/9953))
|
||||||
- Use Clipboard API consistently across the application (by @MilosM348) [#6514](https://github.com/penpot/penpot/issues/6514) (PR: [#9188](https://github.com/penpot/penpot/pull/9188))
|
- Use Clipboard API consistently across the application (by @MilosM348) [#6514](https://github.com/penpot/penpot/issues/6514) (PR: [#9188](https://github.com/penpot/penpot/pull/9188))
|
||||||
- Use `$` as DTCG token/group discriminator and make `$description` optional [#8342](https://github.com/penpot/penpot/issues/8342) (PR: [#9912](https://github.com/penpot/penpot/pull/9912))
|
- Use `$` as DTCG token/group discriminator and make `$description` optional [#8342](https://github.com/penpot/penpot/issues/8342) (PR: [#9912](https://github.com/penpot/penpot/pull/9912))
|
||||||
@ -34,53 +82,32 @@
|
|||||||
- Add composite typography token input to the Design sidebar [#9932](https://github.com/penpot/penpot/issues/9932) (PR: [#9128](https://github.com/penpot/penpot/pull/9128), [#9375](https://github.com/penpot/penpot/pull/9375), [#8749](https://github.com/penpot/penpot/pull/8749))
|
- Add composite typography token input to the Design sidebar [#9932](https://github.com/penpot/penpot/issues/9932) (PR: [#9128](https://github.com/penpot/penpot/pull/9128), [#9375](https://github.com/penpot/penpot/pull/9375), [#8749](https://github.com/penpot/penpot/pull/8749))
|
||||||
- Avoid deduplicating temporary export files to prevent stale content (by @yong2bba) [#9970](https://github.com/penpot/penpot/issues/9970) (PR: [#9959](https://github.com/penpot/penpot/pull/9959))
|
- Avoid deduplicating temporary export files to prevent stale content (by @yong2bba) [#9970](https://github.com/penpot/penpot/issues/9970) (PR: [#9959](https://github.com/penpot/penpot/pull/9959))
|
||||||
- Add layer blur effect [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034))
|
- Add layer blur effect [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034))
|
||||||
- Show and manage comments while designing in the workspace [#10239](https://github.com/penpot/penpot/issues/10239) (PR: [#10275](https://github.com/penpot/penpot/pull/10275))
|
|
||||||
- Add concurrency limiter for MCP Server Plugin Communications [#9493](https://github.com/penpot/penpot/issues/9493) (PR: [#9748](https://github.com/penpot/penpot/pull/9748))
|
- Add concurrency limiter for MCP Server Plugin Communications [#9493](https://github.com/penpot/penpot/issues/9493) (PR: [#9748](https://github.com/penpot/penpot/pull/9748))
|
||||||
- Render guides in WebGL [#10068](https://github.com/penpot/penpot/issues/10068) (PR: [#10014](https://github.com/penpot/penpot/pull/10014))
|
- Render guides in WebGL [#10068](https://github.com/penpot/penpot/issues/10068) (PR: [#10014](https://github.com/penpot/penpot/pull/10014))
|
||||||
- Add configurable resource limits to ImageMagick image processing [#10223](https://github.com/penpot/penpot/issues/10223) (PR: [#10240](https://github.com/penpot/penpot/pull/10240))
|
- Add configurable resource limits to ImageMagick image processing [#10223](https://github.com/penpot/penpot/issues/10223) (PR: [#10240](https://github.com/penpot/penpot/pull/10240))
|
||||||
- Add resource limits to font processing child processes [#10234](https://github.com/penpot/penpot/issues/10234) (PR: [#10274](https://github.com/penpot/penpot/pull/10274))
|
- Add resource limits to font processing child processes [#10234](https://github.com/penpot/penpot/issues/10234) (PR: [#10274](https://github.com/penpot/penpot/pull/10274))
|
||||||
- Add color variants and positioning to selection size badge (by @bittoby) [#10258](https://github.com/penpot/penpot/issues/10258) (PR: [#9210](https://github.com/penpot/penpot/pull/9210))
|
- Add color variants and positioning to selection size badge [#10258](https://github.com/penpot/penpot/issues/10258) (PR: [#9210](https://github.com/penpot/penpot/pull/9210))
|
||||||
- Use hard reload for render engine switching in the workspace menu [#10441](https://github.com/penpot/penpot/issues/10441) (PR: [#10444](https://github.com/penpot/penpot/pull/10444))
|
- Use hard reload for render engine switching in the workspace menu [#10441](https://github.com/penpot/penpot/issues/10441) (PR: [#10444](https://github.com/penpot/penpot/pull/10444))
|
||||||
- Rotate size badge when shape is rotated [#10386](https://github.com/penpot/penpot/issues/10386) (PR: [#10393](https://github.com/penpot/penpot/pull/10393))
|
- Rotate size badge when shape is rotated [#10386](https://github.com/penpot/penpot/issues/10386) (PR: [#10393](https://github.com/penpot/penpot/pull/10393))
|
||||||
|
- Add separate internal URI for exporter to handle Docker deployments where internal and public URIs differ [#10627](https://github.com/penpot/penpot/issues/10627) (PR: [#10630](https://github.com/penpot/penpot/pull/10630))
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
- Fix LDAP provider params schema typo (`bind-passwor` → `bind-password`) introduced during the `clojure.spec` → `malli` migration; the schema slot now matches the runtime key actually read by `prepare-params` (`:password (:bind-password cfg)`) and `try-connectivity` (`(:bind-password cfg)`), so a wrong type for the password no longer slips through unvalidated
|
- Fix Plugin API variant creation failing due to undocumented multi-step workflow [#10075](https://github.com/penpot/penpot/issues/10075) (PR: [#10149](https://github.com/penpot/penpot/pull/10149))
|
||||||
- Fix `login-with-ldap` silently dropping its error message on the `ldap-not-initialized` restriction (typo `:hide` → `:hint`); the message `"ldap auth provider is not initialized"` now actually surfaces in logs and error responses instead of being discarded into an unread key
|
- Fix workspace crash when editing text shapes with degenerate selrect [#10617](https://github.com/penpot/penpot/issues/10617) (PR: [#10618](https://github.com/penpot/penpot/pull/10618))
|
||||||
- Fix `get-view-only-bundle` crashing when a share-link viewer encounters a team member whose email lacks `@` (NullPointerException in `obfuscate-email`) or whose domain has no `.` (previously produced a dangling-dot `****@****.`); now the viewer-side obfuscation is nil-safe and omits the trailing dot when the domain has no TLD
|
|
||||||
- Fix Copy as SVG: emit a single valid SVG document when multiple shapes are selected, and publish `image/svg+xml` to the clipboard so the paste target works in Inkscape and other SVG-native tools [Github #838](https://github.com/penpot/penpot/issues/838)
|
|
||||||
- Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582)
|
|
||||||
- Fix styles between grid layout inputs [Taiga #13526](https://tree.taiga.io/project/penpot/issue/13526)
|
|
||||||
- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534)
|
|
||||||
- Update copy on penpot update message [Taiga #12924](https://tree.taiga.io/project/penpot/issue/12924)
|
|
||||||
- Fix scroll on library modal [Taiga #13639](https://tree.taiga.io/project/penpot/issue/13639)
|
|
||||||
- Fix dates to avoid show them in english when browser is in auto [Taiga #13786](https://tree.taiga.io/project/penpot/issue/13786)
|
|
||||||
- Fix focus radio button [Taiga #13841](https://tree.taiga.io/project/penpot/issue/13841)
|
|
||||||
- Token tree should be expanded by default [Taiga #13631](https://tree.taiga.io/project/penpot/issue/13631)
|
|
||||||
- Fix opacity incorrectly disabled for visible shapes [Taiga #13906](https://tree.taiga.io/project/penpot/issue/13906)
|
|
||||||
- Update onboarding image [Taiga #13864](https://tree.taiga.io/project/penpot/issue/13864)
|
|
||||||
- Fix plugin modal drag interactions over iframe and close-button behavior (by @marekhrabe) [Github #8871](https://github.com/penpot/penpot/pull/8871)
|
|
||||||
- Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923)
|
|
||||||
- Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930)
|
|
||||||
- Display resolved values of inactive tokens [Taiga #13628](https://tree.taiga.io/project/penpot/issue/13628)
|
|
||||||
- Fix app crash when selecting shapes with one hidden [Taiga #13959](https://tree.taiga.io/project/penpot/issue/13959)
|
|
||||||
- Fix opacity mixed value [Taiga #13960](https://tree.taiga.io/project/penpot/issue/13960)
|
|
||||||
- Fix gap input throwing an error [Github #8984](https://github.com/penpot/penpot/pull/8984)
|
|
||||||
- Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990)
|
|
||||||
- Fix colorpicker layout so the eyedropper button is visible again [Taiga #14057](https://tree.taiga.io/project/penpot/issue/14057)
|
|
||||||
- Fix SVG stroke line join not applied when pasting strokes [#4836](https://github.com/penpot/penpot/issues/4836) (PR: [#9982](https://github.com/penpot/penpot/pull/9982), [#10019](https://github.com/penpot/penpot/pull/10019))
|
- Fix SVG stroke line join not applied when pasting strokes [#4836](https://github.com/penpot/penpot/issues/4836) (PR: [#9982](https://github.com/penpot/penpot/pull/9982), [#10019](https://github.com/penpot/penpot/pull/10019))
|
||||||
- Fix blend-mode hover preview on canvas not reverted when dismissing dropdown (by @jack-stormentswe) [#9235](https://github.com/penpot/penpot/issues/9235) (PR: [#9237](https://github.com/penpot/penpot/pull/9237))
|
- Fix blend-mode hover preview on canvas not reverted when dismissing dropdown (by @davidv399) [#9235](https://github.com/penpot/penpot/issues/9235) (PR: [#9237](https://github.com/penpot/penpot/pull/9237))
|
||||||
- Fix View Mode mouse-leave and click in combination not working [#4855](https://github.com/penpot/penpot/issues/4855) (PR: [#9991](https://github.com/penpot/penpot/pull/9991))
|
- Fix View Mode mouse-leave and click in combination not working [#4855](https://github.com/penpot/penpot/issues/4855) (PR: [#9991](https://github.com/penpot/penpot/pull/9991))
|
||||||
- Fix Storybook UI missing scrollbar (by @MilosM348) [#6049](https://github.com/penpot/penpot/issues/6049) (PR: [#9319](https://github.com/penpot/penpot/pull/9319))
|
- Fix Storybook UI missing scrollbar (by @MilosM348) [#6049](https://github.com/penpot/penpot/issues/6049) (PR: [#9319](https://github.com/penpot/penpot/pull/9319))
|
||||||
- Fix font selector missing intermediate font weights for Source Sans Pro and similar fonts (by @dhgoal) [#7378](https://github.com/penpot/penpot/issues/7378) (PR: [#9247](https://github.com/penpot/penpot/pull/9247))
|
- Fix font selector missing intermediate font weights for Source Sans Pro and similar fonts (by @dhgoal) [#7378](https://github.com/penpot/penpot/issues/7378) (PR: [#9247](https://github.com/penpot/penpot/pull/9247))
|
||||||
- Fix plugin API `typography.remove()` passing wrong parameter format (by @leonaIee) [#8223](https://github.com/penpot/penpot/issues/8223) (PR: [#9279](https://github.com/penpot/penpot/pull/9279))
|
- Fix plugin API `typography.remove()` passing wrong parameter format (by @peter-rango) [#8223](https://github.com/penpot/penpot/issues/8223) (PR: [#9279](https://github.com/penpot/penpot/pull/9279))
|
||||||
- Fix plugin API fills and strokes array elements being read-only (by @RenzoMXD) [#8357](https://github.com/penpot/penpot/issues/8357) (PR: [#9161](https://github.com/penpot/penpot/pull/9161))
|
- Fix plugin API fills and strokes array elements being read-only (by @RenzoMXD) [#8357](https://github.com/penpot/penpot/issues/8357) (PR: [#9161](https://github.com/penpot/penpot/pull/9161))
|
||||||
- Fix "Show Guides" shortcut not working on German keyboards (by @RenzoMXD) [#8423](https://github.com/penpot/penpot/issues/8423) (PR: [#9209](https://github.com/penpot/penpot/pull/9209))
|
- Fix "Show Guides" shortcut not working on German keyboards (by @RenzoMXD) [#8423](https://github.com/penpot/penpot/issues/8423) (PR: [#9209](https://github.com/penpot/penpot/pull/9209))
|
||||||
- Fix token validation failing when a malformed token exists in the Component category [#9010](https://github.com/penpot/penpot/issues/9010) (PR: [#9025](https://github.com/penpot/penpot/pull/9025), [#9825](https://github.com/penpot/penpot/pull/9825))
|
- Fix token validation failing when a malformed token exists in the Component category [#9010](https://github.com/penpot/penpot/issues/9010) (PR: [#9025](https://github.com/penpot/penpot/pull/9025), [#9825](https://github.com/penpot/penpot/pull/9825))
|
||||||
- Fix Docker frontend image missing CSS reference (by @NativeTeachingAidsB) [#9135](https://github.com/penpot/penpot/issues/9135) (PR: [#9840](https://github.com/penpot/penpot/pull/9840))
|
- Fix Docker frontend image missing CSS reference (by @NativeTeachingAidsB) [#9135](https://github.com/penpot/penpot/issues/9135) (PR: [#9840](https://github.com/penpot/penpot/pull/9840))
|
||||||
- Fix MCP media upload error and SVG data URI image parsing (by @claytonlin1110) [#9164](https://github.com/penpot/penpot/issues/9164) (PR: [#9201](https://github.com/penpot/penpot/pull/9201))
|
- Fix MCP media upload error and SVG data URI image parsing (by @claytonlin1110) [#9164](https://github.com/penpot/penpot/issues/9164) (PR: [#9201](https://github.com/penpot/penpot/pull/9201))
|
||||||
- Fix lost-update race on team features during concurrent file creation (by @JPette1783) [#9197](https://github.com/penpot/penpot/issues/9197) (PR: [#9198](https://github.com/penpot/penpot/pull/9198))
|
- Fix lost-update race on team features during concurrent file creation (by @Lobster-0429) [#9197](https://github.com/penpot/penpot/issues/9197) (PR: [#9198](https://github.com/penpot/penpot/pull/9198))
|
||||||
- Fix get-profile RPC method silently masking DB errors as "Anonymous User" (by @jack-stormentswe) [#9253](https://github.com/penpot/penpot/issues/9253) (PR: [#9254](https://github.com/penpot/penpot/pull/9254))
|
- Fix get-profile RPC method silently masking DB errors as "Anonymous User" (by @davidv399) [#9253](https://github.com/penpot/penpot/issues/9253) (PR: [#9254](https://github.com/penpot/penpot/pull/9254))
|
||||||
- Fix crash when creating or editing tokens named "white" or "black" [#9256](https://github.com/penpot/penpot/issues/9256) (PR: [#9034](https://github.com/penpot/penpot/pull/9034))
|
- Fix crash when creating or editing tokens named "white" or "black" [#9256](https://github.com/penpot/penpot/issues/9256) (PR: [#9034](https://github.com/penpot/penpot/pull/9034))
|
||||||
- Fix conditional use-ctx hook violation in shape-wrapper (by @Dexterity104) [#9280](https://github.com/penpot/penpot/issues/9280) (PR: [#9281](https://github.com/penpot/penpot/pull/9281))
|
- Fix conditional use-ctx hook violation in shape-wrapper (by @Dexterity104) [#9280](https://github.com/penpot/penpot/issues/9280) (PR: [#9281](https://github.com/penpot/penpot/pull/9281))
|
||||||
- Make ShapeImageIds byte conversion fallible to prevent panics (by @Dexterity104) [#9282](https://github.com/penpot/penpot/issues/9282) (PR: [#9283](https://github.com/penpot/penpot/pull/9283))
|
- Make ShapeImageIds byte conversion fallible to prevent panics (by @Dexterity104) [#9282](https://github.com/penpot/penpot/issues/9282) (PR: [#9283](https://github.com/penpot/penpot/pull/9283))
|
||||||
@ -157,6 +184,19 @@
|
|||||||
- Fix SVG raw caching prevented by unnecessary Cow wrapping [#10488](https://github.com/penpot/penpot/issues/10488) (PR: [#10492](https://github.com/penpot/penpot/pull/10492))
|
- Fix SVG raw caching prevented by unnecessary Cow wrapping [#10488](https://github.com/penpot/penpot/issues/10488) (PR: [#10492](https://github.com/penpot/penpot/pull/10492))
|
||||||
- Add component reset operation to plugin API [#10561](https://github.com/penpot/penpot/issues/10561) (PR: [#10533](https://github.com/penpot/penpot/pull/10533))
|
- Add component reset operation to plugin API [#10561](https://github.com/penpot/penpot/issues/10561) (PR: [#10533](https://github.com/penpot/penpot/pull/10533))
|
||||||
- Fix blur menu alignment in Firefox [#10576](https://github.com/penpot/penpot/issues/10576) (PR: [#10575](https://github.com/penpot/penpot/pull/10575))
|
- Fix blur menu alignment in Firefox [#10576](https://github.com/penpot/penpot/issues/10576) (PR: [#10575](https://github.com/penpot/penpot/pull/10575))
|
||||||
|
- Fix sidebar not showing all elements with grid layout [#10539](https://github.com/penpot/penpot/issues/10539) (PR: [#10600](https://github.com/penpot/penpot/pull/10600))
|
||||||
|
- Fix sidebar getting stuck when selecting shapes that haven't loaded yet [#10599](https://github.com/penpot/penpot/issues/10599) (PR: [#10600](https://github.com/penpot/penpot/pull/10600))
|
||||||
|
- Fix text shape bounding boxes not updating after remote fonts finish loading [#10585](https://github.com/penpot/penpot/issues/10585) (PR: [#10566](https://github.com/penpot/penpot/pull/10566))
|
||||||
|
- Fix text editor crash from Draft.js selection offset exceeding DOM node length [#10607](https://github.com/penpot/penpot/issues/10607) (PR: [#10608](https://github.com/penpot/penpot/pull/10608))
|
||||||
|
- Fix workspace crash when converting SVG-raw shape to path [#10612](https://github.com/penpot/penpot/issues/10612) (PR: [#10613](https://github.com/penpot/penpot/pull/10613))
|
||||||
|
- Fix component variant panel crash when selecting multiple copies with mismatched property counts [#10615](https://github.com/penpot/penpot/issues/10615) (PR: [#10616](https://github.com/penpot/penpot/pull/10616))
|
||||||
|
- Fix workspace crash from recursion when clicking shape in comments mode [#10620](https://github.com/penpot/penpot/issues/10620) (PR: [#10622](https://github.com/penpot/penpot/pull/10622))
|
||||||
|
- Fix Plugin API validation error when listing shared plugin data keys [#10628](https://github.com/penpot/penpot/issues/10628) (PR: [#10632](https://github.com/penpot/penpot/pull/10632))
|
||||||
|
- Fix Plugin API silently dropping plugin data written to shared library [#10629](https://github.com/penpot/penpot/issues/10629) (PR: [#10632](https://github.com/penpot/penpot/pull/10632))
|
||||||
|
- Fix workspace crash when event target is a DOM text node [#10640](https://github.com/penpot/penpot/issues/10640) (PR: [#10641](https://github.com/penpot/penpot/pull/10641))
|
||||||
|
- Fix text shape position-data to include required fills in WASM and DOM calculation paths [#10646](https://github.com/penpot/penpot/issues/10646) (PR: [#10650](https://github.com/penpot/penpot/pull/10650))
|
||||||
|
- Log expired OIDC tokens as auth failures instead of server errors [#10635](https://github.com/penpot/penpot/issues/10635) (PR: [#10636](https://github.com/penpot/penpot/pull/10636))
|
||||||
|
- Return 400 instead of 500 when ImageMagick rejects invalid uploaded images [#10642](https://github.com/penpot/penpot/issues/10642) (PR: [#10643](https://github.com/penpot/penpot/pull/10643))
|
||||||
|
|
||||||
## 2.16.2
|
## 2.16.2
|
||||||
|
|
||||||
|
|||||||
@ -14,9 +14,9 @@ Center](https://help.penpot.app/).
|
|||||||
- [Reporting Bugs](#reporting-bugs)
|
- [Reporting Bugs](#reporting-bugs)
|
||||||
- [Pull Requests](#pull-requests)
|
- [Pull Requests](#pull-requests)
|
||||||
- [Workflow](#workflow)
|
- [Workflow](#workflow)
|
||||||
|
- [Format](#format)
|
||||||
- [Title format](#title-format)
|
- [Title format](#title-format)
|
||||||
- [Description](#description)
|
- [Description](#description)
|
||||||
- [Branch naming](#branch-naming)
|
|
||||||
- [Review process](#review-process)
|
- [Review process](#review-process)
|
||||||
- [What we won't accept](#what-we-wont-accept)
|
- [What we won't accept](#what-we-wont-accept)
|
||||||
- [Good first issues](#good-first-issues)
|
- [Good first issues](#good-first-issues)
|
||||||
@ -73,35 +73,23 @@ Advisories](https://github.com/penpot/penpot/security/advisories)
|
|||||||
4. **Format and lint** — run the checks described in
|
4. **Format and lint** — run the checks described in
|
||||||
[Formatting and Linting](#formatting-and-linting) before submitting.
|
[Formatting and Linting](#formatting-and-linting) before submitting.
|
||||||
|
|
||||||
### Title format
|
### Format
|
||||||
|
|
||||||
|
#### Title
|
||||||
|
|
||||||
|
> **IMPORTANT:** When a PR is squash-merged, the PR title becomes the
|
||||||
|
> commit message on the main branch. Getting the title right matters.
|
||||||
|
|
||||||
Pull request titles **must** follow the same convention as commit subjects:
|
Pull request titles **must** follow the same convention as commit subjects:
|
||||||
|
|
||||||
```
|
```
|
||||||
:emoji: <subject>
|
:emoji: Subject line (imperative, capitalized, no period, <=70 chars)
|
||||||
```
|
```
|
||||||
|
|
||||||
- Use the **imperative mood** (e.g. "Fix", not "Fixed").
|
Read [Creating Commits](./.serena/memories/workflow/creating-commits.md)
|
||||||
- Capitalize the first letter of the subject.
|
for more concrete information.
|
||||||
- Do not end the subject with a period.
|
|
||||||
- Keep the subject to **70 characters** or fewer.
|
|
||||||
- Use one of the [commit type emojis](#commit-types) listed below.
|
|
||||||
|
|
||||||
When a PR contains multiple unrelated commits, choose the emoji that
|
#### Description
|
||||||
best represents the dominant change.
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
|
|
||||||
```
|
|
||||||
:bug: Fix unexpected error on launching modal
|
|
||||||
:sparkles: Enable new modal for profile
|
|
||||||
:zap: Improve performance of dashboard navigation
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Note:** When a PR is squash-merged, the PR title becomes the
|
|
||||||
> commit message on the main branch. Getting the title right matters.
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
Every pull request should include a description that helps reviewers
|
Every pull request should include a description that helps reviewers
|
||||||
understand the change quickly:
|
understand the change quickly:
|
||||||
@ -114,24 +102,8 @@ understand the change quickly:
|
|||||||
5. **Breaking changes** — call out anything that affects existing users
|
5. **Breaking changes** — call out anything that affects existing users
|
||||||
or requires migration steps.
|
or requires migration steps.
|
||||||
|
|
||||||
### Branch naming
|
Read [Creating PRs](./.serena/memories/workflow/creating-prs.md)
|
||||||
|
for more concrete information.
|
||||||
Use a descriptive branch name that reflects the type and scope of the
|
|
||||||
change:
|
|
||||||
|
|
||||||
```
|
|
||||||
<type>/<short-description>
|
|
||||||
```
|
|
||||||
|
|
||||||
Types: `fix`, `feat`, `refactor`, `docs`, `chore`, `perf`.
|
|
||||||
|
|
||||||
Optionally include the issue number:
|
|
||||||
|
|
||||||
```
|
|
||||||
fix/9122-email-blacklisting
|
|
||||||
feat/export-webp
|
|
||||||
refactor/layout-sizing
|
|
||||||
```
|
|
||||||
|
|
||||||
### Review process
|
### Review process
|
||||||
|
|
||||||
@ -151,10 +123,10 @@ refactor/layout-sizing
|
|||||||
To save time on both sides, please avoid submitting PRs that:
|
To save time on both sides, please avoid submitting PRs that:
|
||||||
|
|
||||||
- Introduce new dependencies without prior discussion.
|
- Introduce new dependencies without prior discussion.
|
||||||
- Change the build system or CI configuration without maintainer
|
- Change the build system or CI configuration without maintainer approval.
|
||||||
approval.
|
- Mix unrelated changes in a single PR — keep PRs focused on one concern.
|
||||||
- Mix unrelated changes in a single PR — keep PRs focused on one
|
- Submit AI-generated code without human review.
|
||||||
concern.
|
- Skip local syntax and formatting checks before submitting.
|
||||||
- Skip the [discussion step](#workflow) for non-bug-fix changes.
|
- Skip the [discussion step](#workflow) for non-bug-fix changes.
|
||||||
|
|
||||||
### Good first issues
|
### Good first issues
|
||||||
@ -217,36 +189,8 @@ Commit messages must follow this format:
|
|||||||
|
|
||||||
## Formatting and Linting
|
## Formatting and Linting
|
||||||
|
|
||||||
We use [cljfmt](https://github.com/weavejester/cljfmt) for formatting and
|
Each module has its own linting and formatting commands — see the relevant one on the
|
||||||
[clj-kondo](https://github.com/clj-kondo/clj-kondo) for linting.
|
[Serena Memories](./.serena/memories/)
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check formatting (does not modify files)
|
|
||||||
./scripts/check-fmt
|
|
||||||
|
|
||||||
# Fix formatting (modifies files in place)
|
|
||||||
./scripts/fmt
|
|
||||||
|
|
||||||
# Lint
|
|
||||||
./scripts/lint
|
|
||||||
```
|
|
||||||
|
|
||||||
For frontend SCSS, we use `stylelint` for linting and
|
|
||||||
`Prettier` for formatting:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
|
|
||||||
# Lint SCSS
|
|
||||||
pnpm run lint:scss (does not modify files)
|
|
||||||
|
|
||||||
# Fix SCSS formatting (modifies files in place)
|
|
||||||
pnpm run fmt:scss
|
|
||||||
```
|
|
||||||
|
|
||||||
Ideally, run these as git pre-commit hooks.
|
|
||||||
[Husky](https://typicode.github.io/husky/#/) is a convenient option for
|
|
||||||
setting this up.
|
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
org.clojure/clojure {:mvn/version "1.12.5"}
|
org.clojure/clojure {:mvn/version "1.12.5"}
|
||||||
org.clojure/tools.namespace {:mvn/version "1.5.1"}
|
org.clojure/tools.namespace {:mvn/version "1.5.1"}
|
||||||
|
|
||||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-11"}
|
com.github.luben/zstd-jni {:mvn/version "1.5.7-12"}
|
||||||
|
|
||||||
io.prometheus/simpleclient {:mvn/version "0.16.0"}
|
io.prometheus/simpleclient {:mvn/version "0.16.0"}
|
||||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
|
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
|
||||||
@ -34,27 +34,28 @@
|
|||||||
:exclusions [org.slf4j/slf4j-api]}
|
:exclusions [org.slf4j/slf4j-api]}
|
||||||
|
|
||||||
com.github.seancorfield/next.jdbc
|
com.github.seancorfield/next.jdbc
|
||||||
{:mvn/version "1.3.1108"}
|
{:mvn/version "1.3.1118"}
|
||||||
|
|
||||||
metosin/reitit-core {:mvn/version "0.10.1"}
|
metosin/reitit-core {:mvn/version "0.10.1"}
|
||||||
nrepl/nrepl {:mvn/version "1.7.0"}
|
nrepl/nrepl {:mvn/version "1.7.0"}
|
||||||
|
|
||||||
org.postgresql/postgresql {:mvn/version "42.7.12"}
|
org.postgresql/postgresql {:mvn/version "42.7.13"}
|
||||||
org.xerial/sqlite-jdbc {:mvn/version "3.53.2.0"}
|
org.xerial/sqlite-jdbc {:mvn/version "3.53.2.1"}
|
||||||
|
|
||||||
com.zaxxer/HikariCP {:mvn/version "7.0.2"}
|
com.zaxxer/HikariCP {:mvn/version "7.1.0"}
|
||||||
|
|
||||||
io.whitfin/siphash {:mvn/version "2.0.0"}
|
io.whitfin/siphash {:mvn/version "2.0.0"}
|
||||||
|
|
||||||
buddy/buddy-hashers {:mvn/version "2.0.167"}
|
buddy/buddy-hashers {:mvn/version "2.0.167"}
|
||||||
buddy/buddy-sign {:mvn/version "3.6.1-359"}
|
buddy/buddy-sign {:mvn/version "3.6.1-359"}
|
||||||
|
org.passay/passay {:mvn/version "1.6.6"}
|
||||||
|
|
||||||
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.4"}
|
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.4"}
|
||||||
|
|
||||||
org.jsoup/jsoup {:mvn/version "1.22.2"}
|
org.jsoup/jsoup {:mvn/version "1.23.1"}
|
||||||
|
|
||||||
at.yawk.lz4/lz4-java
|
at.yawk.lz4/lz4-java
|
||||||
{:mvn/version "1.11.0"}
|
{:mvn/version "1.11.1"}
|
||||||
|
|
||||||
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
||||||
|
|
||||||
@ -63,8 +64,8 @@
|
|||||||
|
|
||||||
;; Pretty Print specs
|
;; Pretty Print specs
|
||||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||||
software.amazon.awssdk/s3 {:mvn/version "2.46.18"}
|
software.amazon.awssdk/s3 {:mvn/version "2.50.1"}
|
||||||
software.amazon.awssdk/sts {:mvn/version "2.46.18"}}
|
software.amazon.awssdk/sts {:mvn/version "2.50.1"}}
|
||||||
|
|
||||||
:paths ["src" "resources" "target/classes"]
|
:paths ["src" "resources" "target/classes"]
|
||||||
:aliases
|
:aliases
|
||||||
|
|||||||
@ -104,24 +104,20 @@
|
|||||||
[]
|
[]
|
||||||
(try
|
(try
|
||||||
(main/start)
|
(main/start)
|
||||||
:started
|
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(ex/print-throwable cause))))
|
(ex/print-throwable cause))))
|
||||||
|
|
||||||
(defn- stop
|
(defn- stop
|
||||||
[]
|
[]
|
||||||
(main/stop)
|
(main/stop))
|
||||||
:stopped)
|
|
||||||
|
|
||||||
(defn restart
|
(defn restart
|
||||||
[]
|
[]
|
||||||
(stop)
|
(main/restart))
|
||||||
(repl/refresh :after 'user/start))
|
|
||||||
|
|
||||||
(defn restart-all
|
(defn restart-all
|
||||||
[]
|
[]
|
||||||
(stop)
|
(main/restart-all))
|
||||||
(repl/refresh-all :after 'user/start))
|
|
||||||
|
|
||||||
;; (defn compression-bench
|
;; (defn compression-bench
|
||||||
;; [data]
|
;; [data]
|
||||||
|
|||||||
@ -4,23 +4,25 @@
|
|||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"author": "Kaleidos INC Sucursal en España SL",
|
"author": "Kaleidos INC Sucursal en España SL",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/penpot/penpot"
|
"url": "https://github.com/penpot/penpot"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"luxon": "^3.4.4",
|
"eventsource-parser": "^3.0.6",
|
||||||
"sax": "^1.6.0"
|
"luxon": "^3.7.2",
|
||||||
|
"sax": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.14",
|
"nodemon": "^3.1.14",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"ws": "^8.21.0"
|
"ws": "^8.21.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "clj-kondo --parallel --lint ../common/src src/",
|
"lint:clj": "clj-kondo --config-dir ../.clj-kondo --lint ../common/src src/",
|
||||||
"check-fmt": "cljfmt check --parallel=true src/ test/",
|
"check-fmt:clj": "cljfmt check --parallel=true src/ test/",
|
||||||
"fmt": "cljfmt fix --parallel=true src/ test/"
|
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||||
|
"test:e2e": "node --test --test-concurrency=1 test/e2e/*.test.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
backend/pnpm-lock.yaml
generated
41
backend/pnpm-lock.yaml
generated
@ -8,12 +8,15 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
eventsource-parser:
|
||||||
|
specifier: ^3.0.6
|
||||||
|
version: 3.1.0
|
||||||
luxon:
|
luxon:
|
||||||
specifier: ^3.4.4
|
specifier: ^3.7.2
|
||||||
version: 3.7.2
|
version: 3.7.2
|
||||||
sax:
|
sax:
|
||||||
specifier: ^1.6.0
|
specifier: ^1.6.1
|
||||||
version: 1.6.0
|
version: 1.6.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
nodemon:
|
nodemon:
|
||||||
specifier: ^3.1.14
|
specifier: ^3.1.14
|
||||||
@ -22,8 +25,8 @@ importers:
|
|||||||
specifier: ^0.5.21
|
specifier: ^0.5.21
|
||||||
version: 0.5.21
|
version: 0.5.21
|
||||||
ws:
|
ws:
|
||||||
specifier: ^8.21.0
|
specifier: ^8.21.1
|
||||||
version: 8.21.0
|
version: 8.21.1
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -39,9 +42,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
brace-expansion@5.0.7:
|
brace-expansion@5.0.9:
|
||||||
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
|
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
braces@3.0.3:
|
braces@3.0.3:
|
||||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
@ -63,6 +66,10 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
eventsource-parser@3.1.0:
|
||||||
|
resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -130,8 +137,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||||
engines: {node: '>=8.10.0'}
|
engines: {node: '>=8.10.0'}
|
||||||
|
|
||||||
sax@1.6.0:
|
sax@1.6.1:
|
||||||
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
|
resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==}
|
||||||
engines: {node: '>=11.0.0'}
|
engines: {node: '>=11.0.0'}
|
||||||
|
|
||||||
semver@7.8.5:
|
semver@7.8.5:
|
||||||
@ -165,8 +172,8 @@ packages:
|
|||||||
undefsafe@2.0.5:
|
undefsafe@2.0.5:
|
||||||
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||||
|
|
||||||
ws@8.21.0:
|
ws@8.21.1:
|
||||||
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
bufferutil: ^4.0.1
|
bufferutil: ^4.0.1
|
||||||
@ -188,7 +195,7 @@ snapshots:
|
|||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
brace-expansion@5.0.7:
|
brace-expansion@5.0.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 4.0.4
|
balanced-match: 4.0.4
|
||||||
|
|
||||||
@ -216,6 +223,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
supports-color: 5.5.0
|
supports-color: 5.5.0
|
||||||
|
|
||||||
|
eventsource-parser@3.1.0: {}
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range: 5.0.1
|
to-regex-range: 5.0.1
|
||||||
@ -247,7 +256,7 @@ snapshots:
|
|||||||
|
|
||||||
minimatch@10.2.5:
|
minimatch@10.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.7
|
brace-expansion: 5.0.9
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
@ -274,7 +283,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
picomatch: 2.3.2
|
picomatch: 2.3.2
|
||||||
|
|
||||||
sax@1.6.0: {}
|
sax@1.6.1: {}
|
||||||
|
|
||||||
semver@7.8.5: {}
|
semver@7.8.5: {}
|
||||||
|
|
||||||
@ -301,4 +310,4 @@ snapshots:
|
|||||||
|
|
||||||
undefsafe@2.0.5: {}
|
undefsafe@2.0.5: {}
|
||||||
|
|
||||||
ws@8.21.0: {}
|
ws@8.21.1: {}
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
minimumReleaseAgeExclude:
|
||||||
|
- brace-expansion@5.0.8 || 5.0.9
|
||||||
@ -1,10 +0,0 @@
|
|||||||
Hello!
|
|
||||||
|
|
||||||
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization.name|abbreviate:25 }}”.
|
|
||||||
|
|
||||||
Accept invitation using this link:
|
|
||||||
|
|
||||||
{{ public-uri }}/#/auth/verify-token?token={{token}}
|
|
||||||
|
|
||||||
Enjoy!
|
|
||||||
The Penpot team.
|
|
||||||
@ -195,7 +195,8 @@
|
|||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
<div
|
<div
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="20" height="20" style="display:inline-block;vertical-align:middle;">
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="20" height="20"
|
||||||
|
style="display:inline-block;vertical-align:middle;">
|
||||||
<tr>
|
<tr>
|
||||||
<td width="20" height="20" align="center" valign="middle"
|
<td width="20" height="20" align="center" valign="middle"
|
||||||
background="{% if organization.logo %}{{organization.logo}}{% else %}{{organization.avatar-bg-url}}{% endif %}"
|
background="{% if organization.logo %}{{organization.logo}}{% else %}{{organization.avatar-bg-url}}{% endif %}"
|
||||||
@ -204,12 +205,35 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<span style="display:inline-block; vertical-align: middle;padding-left:5px;height:20px;line-height: 20px;">
|
<span
|
||||||
|
style="display:inline-block; vertical-align: middle;padding-left:5px;height:20px;line-height: 20px;">
|
||||||
{{ organization.name|abbreviate:50 }}
|
{{ organization.name|abbreviate:50 }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
{% if organization.sso-active %}
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||||
|
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its
|
||||||
|
teams and files now goes through your organization's identity provider.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||||
|
If you can't get in, your account probably isn't in the directory yet.
|
||||||
|
To get access, contact the organization owner.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" vertical-align="middle"
|
<td align="center" vertical-align="middle"
|
||||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
17
backend/resources/app/email/invite-to-organization/en.txt
Normal file
17
backend/resources/app/email/invite-to-organization/en.txt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
Hello!
|
||||||
|
|
||||||
|
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization.name|abbreviate:50 }}”.
|
||||||
|
|
||||||
|
{% if organization.sso-active %}
|
||||||
|
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files now goes
|
||||||
|
through your organization's identity provider.
|
||||||
|
|
||||||
|
If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
Accept invitation using this link:
|
||||||
|
|
||||||
|
{{ public-uri }}/#/auth/verify-token?token={{token}}
|
||||||
|
|
||||||
|
Enjoy!
|
||||||
|
The Penpot team.
|
||||||
@ -186,10 +186,31 @@
|
|||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
<div
|
<div
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||||
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”{% if organization %}
|
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:50 }}”{% if
|
||||||
part of the organization “{{ organization|abbreviate:25 }}”{% endif %}.</div>
|
organization %}
|
||||||
|
part of the organization “{{ organization.name|abbreviate:50 }}”{% endif %}.</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if organization.sso-active %}
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||||
|
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to
|
||||||
|
its teams and files now goes through your organization's identity provider.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||||
|
If you can't get in, your account probably isn't in the directory yet.
|
||||||
|
To get access, contact the organization owner.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" vertical-align="middle"
|
<td align="center" vertical-align="middle"
|
||||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
Hello!
|
Hello!
|
||||||
|
|
||||||
{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:25 }}"{% if organization %}, part of the organization "{{ organization|abbreviate:25 }}"{% endif %}.
|
{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:50 }}"{% if organization %}, part of the organization "{{ organization.name|abbreviate:50 }}"{% endif %}.
|
||||||
|
|
||||||
|
{% if organization.sso-active %}
|
||||||
|
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files now goes
|
||||||
|
through your organization's identity provider.
|
||||||
|
|
||||||
|
If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
Accept invitation using this link:
|
Accept invitation using this link:
|
||||||
|
|
||||||
|
|||||||
231
backend/resources/app/email/organization-setup-sso/en.html
Normal file
231
backend/resources/app/email/organization-setup-sso/en.html
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||||
|
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>
|
||||||
|
</title>
|
||||||
|
<!--[if !mso]><!-- -->
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<!--<![endif]-->
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style type="text/css">
|
||||||
|
#outlook a {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table,
|
||||||
|
td {
|
||||||
|
border-collapse: collapse;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
height: auto;
|
||||||
|
line-height: 100%;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: block;
|
||||||
|
margin: 13px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--[if mso]>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG/>
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if lte mso 11]>
|
||||||
|
<style type="text/css">
|
||||||
|
.mj-outlook-group-fix { width:100% !important; }
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
|
||||||
|
<style type="text/css">
|
||||||
|
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
|
||||||
|
</style>
|
||||||
|
<!--<![endif]-->
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (min-width:480px) {
|
||||||
|
.mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mj-column-px-425 {
|
||||||
|
width: 425px !important;
|
||||||
|
max-width: 425px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
table.mj-full-width-mobile {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.mj-full-width-mobile {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="background-color:#E5E5E5;">
|
||||||
|
<div style="background-color:#E5E5E5;">
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
<table
|
||||||
|
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||||
|
<![endif]-->
|
||||||
|
<div style="margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td
|
||||||
|
class="" style="vertical-align:top;width:600px;"
|
||||||
|
>
|
||||||
|
<![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
|
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||||
|
width="100%">
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="border-collapse:collapse;border-spacing:0px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="width:97px;">
|
||||||
|
<img height="32" src="{{ public-uri }}/images/email/logo-penpot.svg"
|
||||||
|
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||||
|
width="97" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table
|
||||||
|
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||||
|
<![endif]-->
|
||||||
|
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td
|
||||||
|
class="" style="vertical-align:top;width:600px;"
|
||||||
|
>
|
||||||
|
<![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
|
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||||
|
width="100%">
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||||
|
Hi,
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||||
|
"{{ organization-name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its
|
||||||
|
teams and files now goes through your organization's identity provider.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||||
|
If you can't get in, your account probably isn't in the directory yet. To get access, contact the
|
||||||
|
organization owner.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||||
|
The Penpot team.</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "app/email/includes/footer.html" %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
“{{ organization-name|abbreviate:25 }}” uses single sign-on
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
Hi,
|
||||||
|
|
||||||
|
"{{ organization-name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files now goes
|
||||||
|
through your organization's identity provider.
|
||||||
|
|
||||||
|
If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
|
||||||
|
|
||||||
|
The Penpot team.
|
||||||
@ -10,9 +10,10 @@ penpot - error list
|
|||||||
<a href="/dbg"> [BACK]</a>
|
<a href="/dbg"> [BACK]</a>
|
||||||
<h1>Error reports (last 300)</h1>
|
<h1>Error reports (last 300)</h1>
|
||||||
|
|
||||||
<a class="{% if version = 3 %}strong{% endif %}" href="?version=3">[BACKEND ERRORS]</a>
|
<a class="{% if source = 0 %}strong{% endif %}" href="?source=0">[ALL ERRORS]</a>
|
||||||
<a class="{% if version = 4 %}strong{% endif %}" href="?version=4">[FRONTEND ERRORS]</a>
|
<a class="{% if source = 3 %}strong{% endif %}" href="?source=3">[BACKEND ERRORS]</a>
|
||||||
<a class="{% if version = 5 %}strong{% endif %}" href="?version=5">[RLIMIT REPORTS]</a>
|
<a class="{% if source = 4 %}strong{% endif %}" href="?source=4">[FRONTEND ERRORS]</a>
|
||||||
|
<a class="{% if source = 5 %}strong{% endif %}" href="?source=5">[RLIMIT REPORTS]</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main class="horizontal-list">
|
<main class="horizontal-list">
|
||||||
|
|||||||
@ -6,7 +6,7 @@ Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v3)
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<nav>
|
<nav>
|
||||||
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div>
|
<div>[<a href="/dbg/error?source={{source}}">⮜</a>]</div>
|
||||||
<div>[<a href="#head">head</a>]</div>
|
<div>[<a href="#head">head</a>]</div>
|
||||||
<div>[<a href="#props">props</a>]</div>
|
<div>[<a href="#props">props</a>]</div>
|
||||||
<div>[<a href="#context">context</a>]</div>
|
<div>[<a href="#context">context</a>]</div>
|
||||||
|
|||||||
@ -6,11 +6,11 @@ Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v4)
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<nav>
|
<nav>
|
||||||
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div>
|
<div>[<a href="/dbg/error?source={{source}}">⮜</a>]</div>
|
||||||
<div>[<a href="#head">head</a>]</div>
|
<div>[<a href="#head">head</a>]</div>
|
||||||
<div>[<a href="#context">context</a>]</div>
|
<div>[<a href="#context">context</a>]</div>
|
||||||
{% if report %}
|
{% if trace %}
|
||||||
<div>[<a href="#report">report</a>]</div>
|
<div>[<a href="#trace">trace</a>]</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
<main>
|
<main>
|
||||||
@ -20,7 +20,7 @@ Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v4)
|
|||||||
<div class="table-val">
|
<div class="table-val">
|
||||||
<h1><span class="not-important">Hint:</span> <br/> {{hint}}</h1>
|
<h1><span class="not-important">Hint:</span> <br/> {{hint}}</h1>
|
||||||
<h2><span class="not-important">Reported at:</span> <br/> {{created-at}}</h2>
|
<h2><span class="not-important">Reported at:</span> <br/> {{created-at}}</h2>
|
||||||
<h2><span class="not-important">Origin:</span> <br/> {{origin}}</h2>
|
<h2><span class="not-important">Kind:</span> <br/> {{kind}}</h2>
|
||||||
<h2><span class="not-important">HREF:</span> <br/> {{href}}</h2>
|
<h2><span class="not-important">HREF:</span> <br/> {{href}}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -33,11 +33,11 @@ Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v4)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if report %}
|
{% if trace %}
|
||||||
<div class="table-row multiline">
|
<div class="table-row multiline">
|
||||||
<div id="report" class="table-key">REPORT:</div>
|
<div id="trace" class="table-key">TRACE:</div>
|
||||||
<div class="table-val">
|
<div class="table-val">
|
||||||
<pre>{{report}}</pre>
|
<pre>{{trace}}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -6,10 +6,10 @@ Report: {{hint|abbreviate:150}} - {{id}} - Penpot Rate Limit Report
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<nav>
|
<nav>
|
||||||
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div>
|
<div>[<a href="/dbg/error?source={{source}}">⮜</a>]</div>
|
||||||
<div>[<a href="#head">head</a>]</div>
|
<div>[<a href="#head">head</a>]</div>
|
||||||
<div>[<a href="#context">context</a>]</div>
|
<div>[<a href="#context">context</a>]</div>
|
||||||
<div>[<a href="#result">result</a>]</div>
|
<div>[<a href="#value">value</a>]</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main>
|
<main>
|
||||||
<div class="table">
|
<div class="table">
|
||||||
@ -30,9 +30,9 @@ Report: {{hint|abbreviate:150}} - {{id}} - Penpot Rate Limit Report
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-row multiline">
|
<div class="table-row multiline">
|
||||||
<div id="result" class="table-key">RESULT: </div>
|
<div id="value" class="table-key">VALUE: </div>
|
||||||
<div class="table-val">
|
<div class="table-val">
|
||||||
<pre>{{result}}</pre>
|
<pre>{{value}}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -39,4 +39,16 @@
|
|||||||
{:permits 3}
|
{:permits 3}
|
||||||
|
|
||||||
:create-file-snapshot/by-profile
|
:create-file-snapshot/by-profile
|
||||||
{:permits 1 :queue 2 :timeout 60000}}
|
{:permits 1 :queue 2 :timeout 60000}
|
||||||
|
|
||||||
|
:send-user-feedback/global
|
||||||
|
{:permits 4}
|
||||||
|
|
||||||
|
:send-user-feedback/by-profile
|
||||||
|
{:permits 1 :queue 3}
|
||||||
|
|
||||||
|
:import-binfile/global
|
||||||
|
{:permits 4}
|
||||||
|
|
||||||
|
:import-binfile/by-profile
|
||||||
|
{:permits 1 :queue 2}}
|
||||||
|
|||||||
@ -1,11 +1,308 @@
|
|||||||
;; Example rlimit.edn file
|
|
||||||
^{:refresh "30s"}
|
^{:refresh "30s"}
|
||||||
{:default
|
{:default
|
||||||
[[:default :window "200000/h"]]
|
[[:default :window "200000/h"]]
|
||||||
|
|
||||||
;; #{:main/get-teams}
|
;; ═══════════════════════════════════════════════
|
||||||
;; [[:burst :bucket "5/5/5s"]]
|
;; Auth & Identity — public, unauthenticated
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/login-with-password}
|
||||||
|
[[:auth-password :bucket "100/50/1m"]]
|
||||||
|
|
||||||
;; #{:main/get-profile}
|
#{:main/login-with-ldap}
|
||||||
;; [[:burst :bucket "60/60/1m"]]
|
[[:auth-ldap :bucket "20/10/5m"]]
|
||||||
}
|
|
||||||
|
#{:main/register-profile}
|
||||||
|
[[:auth-register :bucket "20/10/15m"]]
|
||||||
|
|
||||||
|
#{:main/request-profile-recovery
|
||||||
|
:main/prepare-register-profile}
|
||||||
|
[[:auth-recovery :bucket "100/50/5m"]]
|
||||||
|
|
||||||
|
#{:main/recover-profile
|
||||||
|
:main/verify-token}
|
||||||
|
[[:auth-token :bucket "100/50/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; SSRF vectors — URL fetch endpoints
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/create-file-media-object-from-url}
|
||||||
|
[[:url-fetch :bucket "100/50/5m"]]
|
||||||
|
|
||||||
|
#{:main/create-webhook
|
||||||
|
:main/update-webhook}
|
||||||
|
[[:webhook-validation :bucket "20/10/5m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Search — full sequential scan risk
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/search-files}
|
||||||
|
[[:search :bucket "60/30/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Feedback & Invitations — email-sending
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/send-user-feedback
|
||||||
|
:main/create-team-invitations}
|
||||||
|
[[:email-send :bucket "30/15/5m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Media & File heavy ops
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/upload-file-media-object}
|
||||||
|
[[:image-upload :bucket "200/100/1m"]]
|
||||||
|
|
||||||
|
#{:main/create-file-object-thumbnail
|
||||||
|
:main/delete-file-object-thumbnails
|
||||||
|
:main/get-file-object-thumbnails}
|
||||||
|
[[:thumbnail-ops :bucket "5000/3000/1m"]]
|
||||||
|
|
||||||
|
#{:main/get-file-data-for-thumbnail
|
||||||
|
:main/create-file-thumbnail}
|
||||||
|
[[:thumbnail-data :bucket "100/50/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; UI navigation reads — high frequency
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/get-teams}
|
||||||
|
[[:get-teams :bucket "5000/2500/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-team-members}
|
||||||
|
[[:get-team-members :bucket "4000/2000/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-profile}
|
||||||
|
[[:get-profile :bucket "500/250/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-font-variants}
|
||||||
|
[[:get-font-variants :bucket "250/125/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-comment-threads}
|
||||||
|
[[:get-comment-threads :bucket "500/250/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-profiles-for-file-comments}
|
||||||
|
[[:get-profiles-for-file-comments :bucket "300/150/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-file-libraries}
|
||||||
|
[[:get-file-libraries :bucket "200/100/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-projects}
|
||||||
|
[[:get-projects :bucket "120/60/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-team-recent-files
|
||||||
|
:main/get-unread-comment-threads}
|
||||||
|
[[:get-team-recent :bucket "120/60/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-page}
|
||||||
|
[[:get-page :bucket "150/75/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-access-tokens
|
||||||
|
:main/get-subscription-usage}
|
||||||
|
[[:get-access-tokens :bucket "150/75/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-enabled-flags}
|
||||||
|
[[:get-enabled-flags :bucket "250/125/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-builtin-templates}
|
||||||
|
[[:get-builtin-templates :bucket "200/100/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-project
|
||||||
|
:main/get-project-files}
|
||||||
|
[[:get-project-info :bucket "80/40/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-file}
|
||||||
|
[[:get-file :bucket "180/90/1m"]]
|
||||||
|
|
||||||
|
#{:main/get-team-shared-files
|
||||||
|
:main/get-team-info
|
||||||
|
:main/get-team-users
|
||||||
|
:main/get-team-invitations
|
||||||
|
:main/get-team-deleted-files
|
||||||
|
:main/get-sso-provider}
|
||||||
|
[[:get-team-info :bucket "60/30/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-comments
|
||||||
|
:main/get-file-snapshots
|
||||||
|
:main/get-library-usage
|
||||||
|
:main/has-file-libraries}
|
||||||
|
[[:get-misc-list :bucket "300/150/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-comment-thread
|
||||||
|
:main/get-library-file-references}
|
||||||
|
[[:get-misc-single :bucket "60/30/30s"]]
|
||||||
|
|
||||||
|
#{:main/get-file-info
|
||||||
|
:main/get-view-only-bundle
|
||||||
|
:main/get-all-projects
|
||||||
|
:main/get-owned-teams
|
||||||
|
:main/get-team-stats
|
||||||
|
:main/get-file-summary
|
||||||
|
:main/get-file-stats
|
||||||
|
:main/get-file-fragment}
|
||||||
|
[[:get-light :bucket "60/30/30s"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; File mutations — editing active
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/update-file}
|
||||||
|
[[:update-file :bucket "1000/500/1m"]]
|
||||||
|
|
||||||
|
#{:main/create-file
|
||||||
|
:main/rename-file
|
||||||
|
:main/duplicate-file
|
||||||
|
:main/move-files}
|
||||||
|
[[:file-create :bucket "60/30/1m"]]
|
||||||
|
|
||||||
|
#{:main/delete-file}
|
||||||
|
[[:file-delete :bucket "80/40/1m"]]
|
||||||
|
|
||||||
|
#{:main/set-file-shared
|
||||||
|
:main/update-file-library-sync-status
|
||||||
|
:main/ignore-file-library-sync-status
|
||||||
|
:main/link-file-to-library
|
||||||
|
:main/unlink-file-from-library
|
||||||
|
:main/create-file-snapshot
|
||||||
|
:main/restore-file-snapshot
|
||||||
|
:main/update-file-snapshot
|
||||||
|
:main/delete-file-snapshot
|
||||||
|
:main/lock-file-snapshot
|
||||||
|
:main/unlock-file-snapshot}
|
||||||
|
[[:file-mutations :bucket "80/40/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Project mutations
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/create-project}
|
||||||
|
[[:project-create :bucket "100/50/1m"]]
|
||||||
|
|
||||||
|
#{:main/delete-project
|
||||||
|
:main/rename-project
|
||||||
|
:main/duplicate-project
|
||||||
|
:main/move-project
|
||||||
|
:main/update-project-pin}
|
||||||
|
[[:project-mutations :bucket "40/20/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Team mutations
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/create-team
|
||||||
|
:main/update-team
|
||||||
|
:main/delete-team
|
||||||
|
:main/update-team-photo
|
||||||
|
:main/update-team-member-role
|
||||||
|
:main/delete-team-member
|
||||||
|
:main/leave-team
|
||||||
|
:main/create-team-with-invitations
|
||||||
|
:main/create-team-access-request
|
||||||
|
:main/permanently-delete-team-files
|
||||||
|
:main/restore-deleted-team-files}
|
||||||
|
[[:team-mutations :bucket "60/30/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Comment operations
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/create-comment-thread
|
||||||
|
:main/create-comment
|
||||||
|
:main/update-comment
|
||||||
|
:main/delete-comment
|
||||||
|
:main/mark-all-threads-as-read}
|
||||||
|
[[:comment-basic :bucket "30/15/1m"]]
|
||||||
|
|
||||||
|
#{:main/update-comment-thread
|
||||||
|
:main/update-comment-thread-status
|
||||||
|
:main/update-comment-thread-position
|
||||||
|
:main/update-comment-thread-frame
|
||||||
|
:main/delete-comment-thread}
|
||||||
|
[[:comment-thread :bucket "80/40/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Profile operations
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/update-profile
|
||||||
|
:main/update-profile-props
|
||||||
|
:main/update-profile-photo
|
||||||
|
:main/update-profile-password
|
||||||
|
:main/update-profile-notifications
|
||||||
|
:main/delete-profile
|
||||||
|
:main/delete-profile-photo
|
||||||
|
:main/request-email-change}
|
||||||
|
[[:profile-mutations :bucket "30/15/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Font operations
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/create-font-variant
|
||||||
|
:main/delete-font
|
||||||
|
:main/delete-font-variant
|
||||||
|
:main/update-font
|
||||||
|
:main/download-font
|
||||||
|
:main/download-font-family}
|
||||||
|
[[:font-ops :bucket "100/50/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Access tokens
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/create-access-token
|
||||||
|
:main/delete-access-token}
|
||||||
|
[[:access-token :bucket "60/30/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Export / Import
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/export-binfile
|
||||||
|
:main/import-binfile
|
||||||
|
:main/clone-template}
|
||||||
|
[[:export-import :bucket "80/40/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Upload sessions
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/create-upload-session
|
||||||
|
:main/upload-chunk
|
||||||
|
:main/assemble-file-media-object}
|
||||||
|
[[:upload-session :bucket "100/50/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Webhooks
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/get-webhooks
|
||||||
|
:main/delete-webhook}
|
||||||
|
[[:webhook-read :bucket "20/10/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Share links
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/create-share-link
|
||||||
|
:main/delete-share-link}
|
||||||
|
[[:share-link :bucket "10/5/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Organization operations
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/add-team-to-organization
|
||||||
|
:main/remove-team-from-org
|
||||||
|
:main/all-org-members-in-team
|
||||||
|
:main/all-team-members-in-orgs
|
||||||
|
:main/get-owned-organizations-summary
|
||||||
|
:main/get-leave-org-summary
|
||||||
|
:main/leave-org
|
||||||
|
:main/check-org-members
|
||||||
|
:main/get-team-invitation-token
|
||||||
|
:main/delete-team-invitation
|
||||||
|
:main/check-team-external-invitations}
|
||||||
|
[[:org-ops :bucket "20/10/1m"]]
|
||||||
|
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
;; Audit & stats
|
||||||
|
;; ═══════════════════════════════════════════════
|
||||||
|
#{:main/push-audit-events}
|
||||||
|
[[:audit-events :bucket "1000/500/1m"]]
|
||||||
|
|
||||||
|
#{:main/logout
|
||||||
|
:main/get-error-report
|
||||||
|
:main/get-error-reports
|
||||||
|
:main/get-current-mcp-token
|
||||||
|
:main/get-nitrate-connectivity
|
||||||
|
:main/check-nitrate-sso
|
||||||
|
:main/redeem-nitrate-activation-code
|
||||||
|
:main/create-demo-profile
|
||||||
|
:main/get-subscription-warning}
|
||||||
|
[[:misc-light :bucket "100/50/1m"]]}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user