Compare commits
888 Commits
mobile/lin
...
prerelease
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
102f80eecd | ||
|
|
3b91151388 | ||
|
|
33610fee78 | ||
|
|
9d94e1268c | ||
|
|
52d013cb86 | ||
|
|
21da11df12 | ||
|
|
31509d3629 | ||
|
|
ab3d5bc92a | ||
|
|
ae91d7fb55 | ||
|
|
d1510cb91a | ||
|
|
e0afbcfd96 | ||
|
|
9ceb74cf6e | ||
|
|
ba00a25c85 | ||
|
|
3df3b5e145 | ||
|
|
c17fa45692 | ||
|
|
7613de013e | ||
|
|
f3cd0f2091 | ||
|
|
9824a47a86 | ||
|
|
daa126523a | ||
|
|
975bb3b692 | ||
|
|
4081a6f5e5 | ||
|
|
6f17ee17ca | ||
|
|
b7c44d0fd6 | ||
|
|
8f8d3e7712 | ||
|
|
dc2dd2ee6f | ||
|
|
2dfd62adb8 | ||
|
|
9b535b42c3 | ||
|
|
0bac101e61 | ||
|
|
a1183755c1 | ||
|
|
957bfe240d | ||
|
|
38145975ec | ||
|
|
6834f4d669 | ||
|
|
98174089c9 | ||
|
|
be65d295d0 | ||
|
|
97dec0d3c8 | ||
|
|
f3419a4ee2 | ||
|
|
7a4ec20bac | ||
|
|
7e34ddd356 | ||
|
|
30f4a7fa9d | ||
|
|
9870e93c87 | ||
|
|
c48a201378 | ||
|
|
2c9d2a0d63 | ||
|
|
bc8fbe26cc | ||
|
|
2819ebb52d | ||
|
|
de04af9ff0 | ||
|
|
1ce2280a49 | ||
|
|
ead8ff7293 | ||
|
|
3d02350d4a | ||
|
|
f84ebe668c | ||
|
|
36f6a080a6 | ||
|
|
e31ac3fa7c | ||
|
|
dc7f80024f | ||
|
|
3be66e7683 | ||
|
|
7648fc6809 | ||
|
|
033fd2d7e1 | ||
|
|
ed77caf947 | ||
|
|
774e3b7f92 | ||
|
|
d3d6b68bfe | ||
|
|
f23bcaa9d5 | ||
|
|
d0f02c7272 | ||
|
|
73dfe47d9c | ||
|
|
d9c590bf54 | ||
|
|
03e3cdbc65 | ||
|
|
b22579bdeb | ||
|
|
004b5287c7 | ||
|
|
cac4be7d38 | ||
|
|
e64a042f0c | ||
|
|
914cc6503e | ||
|
|
b142cf6cda | ||
|
|
611be5dd45 | ||
|
|
f41386b753 | ||
|
|
717a7db9e7 | ||
|
|
b307cbc7ea | ||
|
|
1d8580cead | ||
|
|
da3f4c3091 | ||
|
|
0e8aa7ddc4 | ||
|
|
a1a7c45068 | ||
|
|
2e061daa5e | ||
|
|
37b62d0941 | ||
|
|
dedf0cd6f8 | ||
|
|
31455d475c | ||
|
|
47c09ffcfa | ||
|
|
1138fc10cf | ||
|
|
2e7e0b520b | ||
|
|
ffe55244e5 | ||
|
|
b7841d0313 | ||
|
|
1889b8762a | ||
|
|
c870fa9de9 | ||
|
|
7904310be8 | ||
|
|
aa9cea78e2 | ||
|
|
5a0e8b3621 | ||
|
|
dab3ffc762 | ||
|
|
dbfb4e5206 | ||
|
|
44768370e0 | ||
|
|
b80c2d002b | ||
|
|
42c184acb9 | ||
|
|
c600bde17a | ||
|
|
88313c029b | ||
|
|
3825e65693 | ||
|
|
1e6fde571e | ||
|
|
da8072ffbd | ||
|
|
0043577695 | ||
|
|
6bb90efad3 | ||
|
|
979171a591 | ||
|
|
407a0d2f7f | ||
|
|
74cb141dd9 | ||
|
|
32bd929ed5 | ||
|
|
7e2fe5e8dc | ||
|
|
3d6e9919b2 | ||
|
|
12923a87eb | ||
|
|
a2fa74ca8d | ||
|
|
d1fa9b2210 | ||
|
|
bb79f12435 | ||
|
|
25f9ea3f97 | ||
|
|
f8b793069d | ||
|
|
f8514556c5 | ||
|
|
8f84164ff7 | ||
|
|
b690302998 | ||
|
|
1a845583ef | ||
|
|
e786bdc398 | ||
|
|
f0b3884b4b | ||
|
|
9e1edfb7df | ||
|
|
3d47469be3 | ||
|
|
c95a4bfc78 | ||
|
|
dd66a24103 | ||
|
|
8ce7420199 | ||
|
|
cbac6116d4 | ||
|
|
e83cfba357 | ||
|
|
0cac66b203 | ||
|
|
7983ee45e1 | ||
|
|
844cd3433a | ||
|
|
ae6bea2b15 | ||
|
|
37481535e7 | ||
|
|
45a4f0a40d | ||
|
|
9a3e33c0d7 | ||
|
|
25d072944e | ||
|
|
cf8a4b6e6a | ||
|
|
55b1ed170b | ||
|
|
6c58e77ebb | ||
|
|
d712891921 | ||
|
|
07224b1dfd | ||
|
|
9bb5c18ef5 | ||
|
|
0e09aec9fd | ||
|
|
117b77d6d7 | ||
|
|
9087add617 | ||
|
|
f607f3dd79 | ||
|
|
a60cef1f3f | ||
|
|
2f260699ee | ||
|
|
aebaf42eed | ||
|
|
f0fcdf41e4 | ||
|
|
c19cd3ce57 | ||
|
|
9a7df9b05d | ||
|
|
ea349af32f | ||
|
|
e6ffa63aaa | ||
|
|
58dcc66e50 | ||
|
|
23a943a05b | ||
|
|
f1a8522b79 | ||
|
|
277967c722 | ||
|
|
afc97220b9 | ||
|
|
902f57a4a4 | ||
|
|
fb5c47d13d | ||
|
|
0b926b86b1 | ||
|
|
9397bdd733 | ||
|
|
5919ad79a1 | ||
|
|
4d02e1edd9 | ||
|
|
5f6f430aed | ||
|
|
52c9849aed | ||
|
|
ea2d334c40 | ||
|
|
5e6efe0f45 | ||
|
|
8e193f4887 | ||
|
|
e5da8818d5 | ||
|
|
e8b6d9a7b0 | ||
|
|
622d97c06f | ||
|
|
43bc27e00f | ||
|
|
6ae701b389 | ||
|
|
c8fb61e270 | ||
|
|
cf1cafa46b | ||
|
|
f9a33a14d6 | ||
|
|
07db8696cb | ||
|
|
76c0e615af | ||
|
|
27a632d186 | ||
|
|
ed11581d5e | ||
|
|
a9c8aff56f | ||
|
|
b82b9f3837 | ||
|
|
a0a1ca4855 | ||
|
|
a28f137bbd | ||
|
|
c16b31267d | ||
|
|
36815ad579 | ||
|
|
e3bc160e3f | ||
|
|
7730789033 | ||
|
|
5c09fae3fc | ||
|
|
15529c3255 | ||
|
|
59e51412d6 | ||
|
|
6f206e5f1a | ||
|
|
04801d2b81 | ||
|
|
e75e9b95aa | ||
|
|
fac96d2e3e | ||
|
|
6fc739a514 | ||
|
|
7402e4bb66 | ||
|
|
a00b584126 | ||
|
|
ad88007829 | ||
|
|
8f5b8aad31 | ||
|
|
dcfd0e2e92 | ||
|
|
0f3716a206 | ||
|
|
b6c52df888 | ||
|
|
e8db6672c1 | ||
|
|
4f9e4581f3 | ||
|
|
456ffa6f5b | ||
|
|
70468a8a64 | ||
|
|
ac766221f2 | ||
|
|
8699fc2865 | ||
|
|
c441df5377 | ||
|
|
612ae400c5 | ||
|
|
7a9a34d142 | ||
|
|
5dabea55b3 | ||
|
|
62eee6404a | ||
|
|
1c13389e4d | ||
|
|
6609580f72 | ||
|
|
63343d32e2 | ||
|
|
57b2c36145 | ||
|
|
d0fc4949be | ||
|
|
74a1182381 | ||
|
|
d1f9d8e125 | ||
|
|
340e49f480 | ||
|
|
29e1af364b | ||
|
|
2c1d5eeeff | ||
|
|
e80805276a | ||
|
|
eeae707229 | ||
|
|
4e243420fa | ||
|
|
73838c8ec6 | ||
|
|
135719f7be | ||
|
|
9cc802cd28 | ||
|
|
4184496010 | ||
|
|
0bc8db4ed5 | ||
|
|
a332889db9 | ||
|
|
9cbcc035ac | ||
|
|
499ee4e58c | ||
|
|
ed9a3a7204 | ||
|
|
326398cc3f | ||
|
|
a77f83ed31 | ||
|
|
8feae65e37 | ||
|
|
015a8d682a | ||
|
|
5671c598b9 | ||
|
|
54f833b9c4 | ||
|
|
82c8e9f98c | ||
|
|
959d2044ca | ||
|
|
48c41e8015 | ||
|
|
3705e27330 | ||
|
|
8c4af1f5ef | ||
|
|
ece822e1c5 | ||
|
|
1062a51226 | ||
|
|
173090c2fd | ||
|
|
a604e19ac4 | ||
|
|
51f93861e4 | ||
|
|
6e4a06528b | ||
|
|
a30b406781 | ||
|
|
88a9ba6cba | ||
|
|
d5b6e40c81 | ||
|
|
feb68dc7f8 | ||
|
|
ad53fcb3c6 | ||
|
|
1bd2964e15 | ||
|
|
4c3368bd13 | ||
|
|
ee4d9d65af | ||
|
|
7dc34a9b99 | ||
|
|
9d646f9d12 | ||
|
|
0bfa988f2b | ||
|
|
37b570c302 | ||
|
|
05e187092a | ||
|
|
531cdc264a | ||
|
|
199ebb499f | ||
|
|
bf8dd4d345 | ||
|
|
ba49c1651e | ||
|
|
ea180dc924 | ||
|
|
ca7164b01c | ||
|
|
25ad8353d8 | ||
|
|
116508aa47 | ||
|
|
4028ba448d | ||
|
|
1c7b8e2e5f | ||
|
|
1434ed8711 | ||
|
|
275e6da9e8 | ||
|
|
bfc1b0ef3f | ||
|
|
f55a67c009 | ||
|
|
cd1b5f1052 | ||
|
|
55e41fbf97 | ||
|
|
e341630e8c | ||
|
|
a0a0d5de51 | ||
|
|
6308598f29 | ||
|
|
ed9999be4c | ||
|
|
7cf8d3a96b | ||
|
|
b5b3b533bf | ||
|
|
a559fda57f | ||
|
|
f8440b62a8 | ||
|
|
11ae860f5c | ||
|
|
d4614ee1ab | ||
|
|
804a09e4ad | ||
|
|
dc43a680e9 | ||
|
|
344448560e | ||
|
|
06aaab0277 | ||
|
|
48bf49c2d5 | ||
|
|
8116279300 | ||
|
|
b020e84b72 | ||
|
|
15b7169529 | ||
|
|
c5285aa512 | ||
|
|
2682f3bca8 | ||
|
|
4fd70cab8f | ||
|
|
677c227f16 | ||
|
|
1e935baf01 | ||
|
|
11b20e7961 | ||
|
|
ee33b5c82b | ||
|
|
89c0254df4 | ||
|
|
7c3084fbc4 | ||
|
|
2fa1d34d17 | ||
|
|
7666337df6 | ||
|
|
af64b7484d | ||
|
|
dcf277afb3 | ||
|
|
56ab44301b | ||
|
|
296ffabee0 | ||
|
|
17057027d5 | ||
|
|
cd724027fb | ||
|
|
0a20a82d01 | ||
|
|
9163c0a64e | ||
|
|
88d63d544e | ||
|
|
c3590d8ce7 | ||
|
|
6b74c58714 | ||
|
|
292877ab83 | ||
|
|
151b409e0e | ||
|
|
b704fbbf7c | ||
|
|
7b7f6c904c | ||
|
|
9fccd1e6dd | ||
|
|
bab4b799d8 | ||
|
|
06ca5a87f4 | ||
|
|
d01528b2cf | ||
|
|
4e041ce5bf | ||
|
|
2ed1add5bc | ||
|
|
553d18e938 | ||
|
|
af720eecac | ||
|
|
d23eb40401 | ||
|
|
b590d68a92 | ||
|
|
dbca4c9962 | ||
|
|
020f9c7dc0 | ||
|
|
11c2ec4255 | ||
|
|
3687e7ae09 | ||
|
|
eafd128c63 | ||
|
|
51bbdcf1c4 | ||
|
|
a2f14ee30b | ||
|
|
ad194555ee | ||
|
|
3d27dd91f0 | ||
|
|
7ca1486bb0 | ||
|
|
2894d2f982 | ||
|
|
a66540d2b2 | ||
|
|
5c3d9bea85 | ||
|
|
df2d60abe1 | ||
|
|
0178e3aed0 | ||
|
|
0b9f01b936 | ||
|
|
f0d775b3ae | ||
|
|
c5ad46cede | ||
|
|
9b3de061a7 | ||
|
|
624de5645b | ||
|
|
969630cad3 | ||
|
|
9ae6481fd4 | ||
|
|
9d90eb5207 | ||
|
|
f2ad60f12e | ||
|
|
dd987fd625 | ||
|
|
d4bb3d043c | ||
|
|
b65fc30ede | ||
|
|
f16e539235 | ||
|
|
4501211125 | ||
|
|
2e9e5017fe | ||
|
|
0aa163f329 | ||
|
|
eb60cc8c5b | ||
|
|
d288d461dc | ||
|
|
5a43fb572f | ||
|
|
31d2d2cdfc | ||
|
|
2e9a9db1b7 | ||
|
|
8cd3099ee4 | ||
|
|
10203bc98a | ||
|
|
3a1671e479 | ||
|
|
ce9f527f63 | ||
|
|
8b538c82d7 | ||
|
|
5256e12a32 | ||
|
|
44790d927e | ||
|
|
a622925fed | ||
|
|
46e2c46476 | ||
|
|
5decb2ab3f | ||
|
|
842bc40d0c | ||
|
|
1faead2732 | ||
|
|
bd2d1cce30 | ||
|
|
3eb57ba407 | ||
|
|
80d1071019 | ||
|
|
c9a8c9c0c0 | ||
|
|
207a0ab496 | ||
|
|
e755c6a260 | ||
|
|
d65ccf4f49 | ||
|
|
628fca09f2 | ||
|
|
fc27456e95 | ||
|
|
1eaf19e572 | ||
|
|
2f5246d535 | ||
|
|
1ed0be4e7a | ||
|
|
fe841ea6ff | ||
|
|
cb024bbca7 | ||
|
|
3ed55fd08a | ||
|
|
ab71e1e33f | ||
|
|
c94398b865 | ||
|
|
4cde4dc5f5 | ||
|
|
903612c905 | ||
|
|
04988a1748 | ||
|
|
1d964978d6 | ||
|
|
b6647427f7 | ||
|
|
7e645fcdbf | ||
|
|
5ddfb30817 | ||
|
|
7afa8d9c23 | ||
|
|
3e46012350 | ||
|
|
6ccee5bc33 | ||
|
|
f5bfa4db8a | ||
|
|
634527b640 | ||
|
|
4044a14da2 | ||
|
|
0dd2c61fe5 | ||
|
|
2b66cabfe0 | ||
|
|
178d00ca06 | ||
|
|
22b607659e | ||
|
|
a5324a2bd8 | ||
|
|
3dba323bc0 | ||
|
|
cfedb8f822 | ||
|
|
d27a135613 | ||
|
|
af19c9d239 | ||
|
|
cfca359f68 | ||
|
|
9933d02139 | ||
|
|
4a8425c40b | ||
|
|
826c1095e6 | ||
|
|
b7f7f49ca9 | ||
|
|
0b8f267f9b | ||
|
|
1c1531ec12 | ||
|
|
9c30602230 | ||
|
|
84ecc7751e | ||
|
|
7e8dfcb691 | ||
|
|
f13cc304ed | ||
|
|
bf0a6bad5c | ||
|
|
99d0b57e0e | ||
|
|
36ecb67bd3 | ||
|
|
a99f9c2e7a | ||
|
|
107f3fa105 | ||
|
|
bfe5414ec0 | ||
|
|
554882d0a8 | ||
|
|
f2ca574d7e | ||
|
|
81b7305f5f | ||
|
|
7e2439f781 | ||
|
|
96f480ef3c | ||
|
|
583dd24423 | ||
|
|
6772a1c7d1 | ||
|
|
49e25effc0 | ||
|
|
7f8f6ee11b | ||
|
|
22d9054d8a | ||
|
|
64a254b12d | ||
|
|
d8ebce866f | ||
|
|
7d0cc155b1 | ||
|
|
baba070baf | ||
|
|
dc492dba35 | ||
|
|
ccf3dd7781 | ||
|
|
2f272b9733 | ||
|
|
c1fedf07b6 | ||
|
|
af2197cfe1 | ||
|
|
c4d5c4e090 | ||
|
|
4ef9dc11e3 | ||
|
|
70854329c2 | ||
|
|
a214ad84f5 | ||
|
|
48c9efb354 | ||
|
|
3e481d2850 | ||
|
|
30a47b48df | ||
|
|
4356a23f9c | ||
|
|
0646689266 | ||
|
|
ba2f767f83 | ||
|
|
c5c5d55782 | ||
|
|
aef82dee24 | ||
|
|
3c2a7e8bf6 | ||
|
|
1211b086d7 | ||
|
|
0fca01f59e | ||
|
|
2ba87486a6 | ||
|
|
0026a02f76 | ||
|
|
67b4001e9c | ||
|
|
9d59672a60 | ||
|
|
4c76cb2bbf | ||
|
|
9ced05fca9 | ||
|
|
d54bdffc6b | ||
|
|
a6f3bcafe4 | ||
|
|
5424d6b402 | ||
|
|
69fe8f61d5 | ||
|
|
f677ee64d4 | ||
|
|
17148c7c70 | ||
|
|
98ea289509 | ||
|
|
5748e279f1 | ||
|
|
552126589f | ||
|
|
c5b39ab0a3 | ||
|
|
600d130d14 | ||
|
|
0b719e0ca1 | ||
|
|
b47fe50bef | ||
|
|
56abc1c3f3 | ||
|
|
4915990761 | ||
|
|
700cf4798c | ||
|
|
761f50ed00 | ||
|
|
dfbcfc25db | ||
|
|
2dbb27fc1b | ||
|
|
fbdd357de4 | ||
|
|
b0cf66a121 | ||
|
|
3311ea58fd | ||
|
|
194d19ef58 | ||
|
|
d90aeffb91 | ||
|
|
7eb0544957 | ||
|
|
cece84e9bc | ||
|
|
4c94d063da | ||
|
|
7360e35ace | ||
|
|
5fe40200f1 | ||
|
|
a562dcd916 | ||
|
|
3c3c3ab463 | ||
|
|
1f2f103fc6 | ||
|
|
a698a2db83 | ||
|
|
c746cf1cf1 | ||
|
|
2032503220 | ||
|
|
8674c5145c | ||
|
|
f80476cef6 | ||
|
|
d26ccb9fe5 | ||
|
|
90c454450f | ||
|
|
de78221ec3 | ||
|
|
85f58ebcb4 | ||
|
|
b724708411 | ||
|
|
88e1c714d4 | ||
|
|
0bfb097bef | ||
|
|
dd788aceae | ||
|
|
fef1bee1c0 | ||
|
|
d5a7d63808 | ||
|
|
4e1e608a1e | ||
|
|
73ce8f511b | ||
|
|
720bb85801 | ||
|
|
f82709cf4d | ||
|
|
30dca60c58 | ||
|
|
2565f5d59d | ||
|
|
9e5f9c2930 | ||
|
|
49ed1ba895 | ||
|
|
eb0c5418c8 | ||
|
|
90e3ffe539 | ||
|
|
6f42b9f6e3 | ||
|
|
e56103d456 | ||
|
|
6878ca5365 | ||
|
|
f7d818529d | ||
|
|
5d87628728 | ||
|
|
5eab5cf9d1 | ||
|
|
d73265cdc7 | ||
|
|
67f76080a9 | ||
|
|
9bacf7f2d7 | ||
|
|
fb064f7ad3 | ||
|
|
7968c18bcb | ||
|
|
f885174a74 | ||
|
|
e997f1dc94 | ||
|
|
483da232d2 | ||
|
|
f3d2a6a90b | ||
|
|
fd8d0cf4f3 | ||
|
|
1a2d1c175e | ||
|
|
b210f45e21 | ||
|
|
8bd12d2456 | ||
|
|
34c268c861 | ||
|
|
3ef647b240 | ||
|
|
8c47ea940a | ||
|
|
b01586ac1d | ||
|
|
0566e9c308 | ||
|
|
f4df1400cc | ||
|
|
fe8df6c57c | ||
|
|
5c1e1b7fd1 | ||
|
|
1cb7459803 | ||
|
|
d5c9624242 | ||
|
|
97703077ef | ||
|
|
287d88fc41 | ||
|
|
70b653fc24 | ||
|
|
fa226311a9 | ||
|
|
941838c045 | ||
|
|
36969de9e3 | ||
|
|
64c455a897 | ||
|
|
f7a4417243 | ||
|
|
f5addccb1e | ||
|
|
8a90292f97 | ||
|
|
39a8f94931 | ||
|
|
f1dccec274 | ||
|
|
a7e862acbb | ||
|
|
cfe53bf0cd | ||
|
|
5dcf7fd2b0 | ||
|
|
b26bfc457b | ||
|
|
3b538b6fff | ||
|
|
f634dc9850 | ||
|
|
46092def6b | ||
|
|
5ccd7a7a32 | ||
|
|
4b4914619c | ||
|
|
0ae15a2eed | ||
|
|
faa1ed0bff | ||
|
|
7a90e1e2c7 | ||
|
|
43d5ef0ac0 | ||
|
|
80c8c665f5 | ||
|
|
cd9721d675 | ||
|
|
03f4f00547 | ||
|
|
9c5e4bc7a1 | ||
|
|
9ee18ad59a | ||
|
|
ae2bedd681 | ||
|
|
638a631405 | ||
|
|
732f1e9391 | ||
|
|
8cebea3a25 | ||
|
|
5787ae51e5 | ||
|
|
8a9803fc5f | ||
|
|
894b137432 | ||
|
|
c1e801c487 | ||
|
|
d32b0af92d | ||
|
|
0137e6b2b3 | ||
|
|
f0e67d71d4 | ||
|
|
181e30e168 | ||
|
|
9a24c1019d | ||
|
|
521aa787ac | ||
|
|
5fedcde883 | ||
|
|
f1f3079b0c | ||
|
|
ce8ea5a534 | ||
|
|
48a4e02097 | ||
|
|
073c78bce5 | ||
|
|
3cc42cd58c | ||
|
|
9bf146cd70 | ||
|
|
067cd5122b | ||
|
|
a446a5fce1 | ||
|
|
2277b12be8 | ||
|
|
c39b8d5018 | ||
|
|
d6b1ba1250 | ||
|
|
b9ea77f7bc | ||
|
|
46f330213f | ||
|
|
e9e213148d | ||
|
|
9233649012 | ||
|
|
dae2eb210b | ||
|
|
4555d62fc7 | ||
|
|
fb40f0b2c6 | ||
|
|
413c9f7919 | ||
|
|
3538723e87 | ||
|
|
b84d3f0a75 | ||
|
|
53d1588b14 | ||
|
|
22ad3e5ff7 | ||
|
|
741460fd10 | ||
|
|
48fbda27fa | ||
|
|
dbb5db590d | ||
|
|
f92c760d8d | ||
|
|
c559e6b68c | ||
|
|
38cbf49ef7 | ||
|
|
51b4090066 | ||
|
|
e722f58b52 | ||
|
|
3817952ed6 | ||
|
|
7fa3a13783 | ||
|
|
216732581a | ||
|
|
bc1a039105 | ||
|
|
268544b6d7 | ||
|
|
be0dc4ecd4 | ||
|
|
8caa78b1f7 | ||
|
|
899801ab0c | ||
|
|
d61dbea744 | ||
|
|
30f2ef6da6 | ||
|
|
9260fc4c69 | ||
|
|
5d433c617e | ||
|
|
b5e8296318 | ||
|
|
7f880446cc | ||
|
|
bf99b244e0 | ||
|
|
2b11c85c7e | ||
|
|
50f31a7cf0 | ||
|
|
83bb5f982a | ||
|
|
c38e8f380a | ||
|
|
9978365350 | ||
|
|
bee714a383 | ||
|
|
ef67722bdb | ||
|
|
d9e593c689 | ||
|
|
d481f15fb2 | ||
|
|
497911bd99 | ||
|
|
c337afa90c | ||
|
|
5895e51594 | ||
|
|
1133af7669 | ||
|
|
d1055d00e6 | ||
|
|
934b2d0a1e | ||
|
|
020079210d | ||
|
|
0a57b4341a | ||
|
|
c6e290268f | ||
|
|
055f2c704b | ||
|
|
3ef6036933 | ||
|
|
a210fba367 | ||
|
|
61d963cf98 | ||
|
|
bdf2f839e3 | ||
|
|
0452acc108 | ||
|
|
b4ea66f5d5 | ||
|
|
2fd7e15892 | ||
|
|
e2f79d3089 | ||
|
|
17b1f15eaf | ||
|
|
67d314c8dd | ||
|
|
374e2a839d | ||
|
|
8fa23838c0 | ||
|
|
f20bed8e9f | ||
|
|
b453e6771b | ||
|
|
c923505ee2 | ||
|
|
1cccf50def | ||
|
|
01d2b1cb5e | ||
|
|
875f90525c | ||
|
|
e7486b1299 | ||
|
|
11f28edbf3 | ||
|
|
4a7c7022ee | ||
|
|
0cd8aa0d3d | ||
|
|
3dfa3d196a | ||
|
|
affb20b763 | ||
|
|
0e9154323a | ||
|
|
58946590d7 | ||
|
|
f18a30fde0 | ||
|
|
5969844407 | ||
|
|
13ecf64ddb | ||
|
|
00cc8bf40b | ||
|
|
b5ae4c5938 | ||
|
|
4985cbff96 | ||
|
|
6453abad31 | ||
|
|
cf0c17a623 | ||
|
|
debbc5a5bc | ||
|
|
d93439dbfe | ||
|
|
533de26936 | ||
|
|
e7aef3a4a7 | ||
|
|
ed2adc8a15 | ||
|
|
3fc8d44614 | ||
|
|
c0937355dc | ||
|
|
eeb45dc5dc | ||
|
|
89cf9a1e2f | ||
|
|
d7274c291f | ||
|
|
e6b9680a39 | ||
|
|
bdfe900828 | ||
|
|
575ec8eda2 | ||
|
|
ba06f4dfaa | ||
|
|
4e27e7772c | ||
|
|
feb78b0cab | ||
|
|
efb0305ecf | ||
|
|
dd3e1fbd47 | ||
|
|
d37c1bfb25 | ||
|
|
1b2102d8e0 | ||
|
|
ccd0ac5e4d | ||
|
|
0566096d1d | ||
|
|
a4fad8c8ca | ||
|
|
352d1bf730 | ||
|
|
9df4cc18eb | ||
|
|
7ed206e178 | ||
|
|
98ac4d7e6a | ||
|
|
b56b00704d | ||
|
|
0d333be6d0 | ||
|
|
22529a911d | ||
|
|
7285753e2d | ||
|
|
291ce8aa58 | ||
|
|
c666f3e448 | ||
|
|
ac93d58035 | ||
|
|
344fb9ac8a | ||
|
|
a3fc674705 | ||
|
|
02fd340e64 | ||
|
|
992bcd730f | ||
|
|
26fa00e3be | ||
|
|
13b09bfa9f | ||
|
|
22f3c5954b | ||
|
|
3ddfcc4dbf | ||
|
|
4cf165475a | ||
|
|
11114e9309 | ||
|
|
49ca125aa2 | ||
|
|
8dc49538e0 | ||
|
|
5196d068ad | ||
|
|
5c344ae62d | ||
|
|
a419fc0822 | ||
|
|
fce29510b0 | ||
|
|
dc249ab8b6 | ||
|
|
944b7a6b24 | ||
|
|
c16f82d1e8 | ||
|
|
ad07361ee1 | ||
|
|
2ccb7e7f67 | ||
|
|
7aa01a658a | ||
|
|
b03ba7d36a | ||
|
|
e41e54b10a | ||
|
|
9075cdde10 | ||
|
|
95659a9b76 | ||
|
|
a44b21859e | ||
|
|
a074c656a6 | ||
|
|
05c15b6112 | ||
|
|
3ed4be0ef4 | ||
|
|
9d783e9e4e | ||
|
|
658c0088ee | ||
|
|
88ef9a1ba3 | ||
|
|
166a5b0b79 | ||
|
|
679fbaffaa | ||
|
|
314adc6815 | ||
|
|
047a645b86 | ||
|
|
25935c8972 | ||
|
|
454588b75a | ||
|
|
2162002931 | ||
|
|
8c4c38b663 | ||
|
|
d67752a378 | ||
|
|
7f8080badc | ||
|
|
bcc6fcccc0 | ||
|
|
851088389a | ||
|
|
af1b69caf5 | ||
|
|
941a6af00e | ||
|
|
d188dacfa1 | ||
|
|
8a4e8a5f99 | ||
|
|
f01091376a | ||
|
|
4996c9d3fa | ||
|
|
fc50203896 | ||
|
|
5d6b0d65c3 | ||
|
|
c77c8be284 | ||
|
|
f312a172b5 | ||
|
|
175cf80927 | ||
|
|
0ac91c006d | ||
|
|
78ab43098d | ||
|
|
9bdd0094e3 | ||
|
|
2843042bb8 | ||
|
|
4f01f3ecdf | ||
|
|
e2bb86f789 | ||
|
|
25429304cc | ||
|
|
03b820f692 | ||
|
|
791f354009 | ||
|
|
bd82f73a7e | ||
|
|
e906e63599 | ||
|
|
d068dde24d | ||
|
|
29f0aa83e4 | ||
|
|
82e84650a1 | ||
|
|
902a7444ed | ||
|
|
7343a695b7 | ||
|
|
481658ebd5 | ||
|
|
77724bbe48 | ||
|
|
3b65fac2dc | ||
|
|
c3b895bb13 | ||
|
|
ab5731f781 | ||
|
|
783082fbaf | ||
|
|
00eda0a34e | ||
|
|
77fcaafffe | ||
|
|
4628d98051 | ||
|
|
949920cbc9 | ||
|
|
dda475b879 | ||
|
|
90d984b034 | ||
|
|
9410aa12d3 | ||
|
|
ccf105aae3 | ||
|
|
25d0b5e793 | ||
|
|
48969f05a1 | ||
|
|
cb3cd41c53 | ||
|
|
17824a222a | ||
|
|
9fcc4a759d | ||
|
|
c3fb555bda | ||
|
|
29d6255749 | ||
|
|
61c5861c31 | ||
|
|
8073c777e8 | ||
|
|
68c0aef4ac | ||
|
|
4febb33833 | ||
|
|
379ef5270c | ||
|
|
a0bc2cfeab | ||
|
|
d143231040 | ||
|
|
032c8d36c0 | ||
|
|
8cc96dfcb1 | ||
|
|
1fa70892aa | ||
|
|
fb78dea5b7 | ||
|
|
a5464d5c38 | ||
|
|
733f285277 | ||
|
|
8b9a4c0101 | ||
|
|
041c1c3a09 | ||
|
|
4e645ada72 | ||
|
|
b4952e25ff | ||
|
|
c63a587751 | ||
|
|
3ad0249ef0 | ||
|
|
c50bcd87e6 | ||
|
|
b2f888985f | ||
|
|
aeda7972fd | ||
|
|
1649a6106d | ||
|
|
a6abfceb16 | ||
|
|
14d9afc768 | ||
|
|
38f50ced4d | ||
|
|
095f10695e | ||
|
|
ae016a3e68 | ||
|
|
1903030b16 | ||
|
|
1f78a5580b | ||
|
|
2973ae8774 | ||
|
|
0d0629b062 | ||
|
|
69771af24d | ||
|
|
8d6c2bb1bc | ||
|
|
6855d2c73d | ||
|
|
3af29d86d5 | ||
|
|
56672de436 | ||
|
|
8af9df42a0 | ||
|
|
869f751968 | ||
|
|
423c3132ab | ||
|
|
3e3417a67b | ||
|
|
09e04d6784 | ||
|
|
0c90ecf4e2 | ||
|
|
3f3b70920d | ||
|
|
28d4ee94dd | ||
|
|
66c7ea2155 | ||
|
|
57da2a3c82 | ||
|
|
6f9ceea363 | ||
|
|
8d4317fa85 |
69
.github/ISSUE_TEMPLATE/documentation.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
name: 'Documentation'
|
||||
description: Report documentation issues, request new documentation, or suggest improvements to existing docs.
|
||||
title: '[Docs] - <title>'
|
||||
labels: ['documentation']
|
||||
body:
|
||||
- type: dropdown
|
||||
id: issue-type
|
||||
attributes:
|
||||
label: 'Issue Type'
|
||||
description: What type of documentation issue is this?
|
||||
options:
|
||||
- New Documentation Request
|
||||
- Documentation Improvement
|
||||
- Documentation Bug/Error
|
||||
- Documentation Change Request
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 'Description'
|
||||
description: Please describe the documentation issue, request, or improvement
|
||||
placeholder: Provide a clear and detailed description...
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: doc-url
|
||||
attributes:
|
||||
label: 'Documentation URL'
|
||||
description: If this relates to existing documentation, please provide the URL
|
||||
placeholder: ex. https://actualbudget.org/docs/budgeting/categories or https://github.com/actualbudget/actual/blob/master/packages/docs/...
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: 'Documentation Category'
|
||||
description: What category does this relate to?
|
||||
multiple: true
|
||||
options:
|
||||
- Accounts
|
||||
- Backup & Restore
|
||||
- Budgeting
|
||||
- Development
|
||||
- Installation & Configuration
|
||||
- Overview
|
||||
- Reports
|
||||
- Troubleshooting
|
||||
- Other
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: 'Expected/Desired Content'
|
||||
description: If applicable, describe what you expect to see or what should be documented
|
||||
placeholder: What should the documentation say or include?
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: screenshot
|
||||
attributes:
|
||||
label: 'Screenshots or Examples'
|
||||
description: If applicable, add screenshots or examples to help explain your request
|
||||
value: |
|
||||

|
||||
render: bash
|
||||
validations:
|
||||
required: false
|
||||
17
.github/actions/docs-spelling/README.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# check-spelling/check-spelling configuration
|
||||
|
||||
| File | Purpose | Format | Info |
|
||||
| -------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| [dictionary.txt](dictionary.txt) | Replacement dictionary (creating this file will override the default dictionary) | one word per line | [dictionary](https://github.com/check-spelling/check-spelling/wiki/Configuration#dictionary) |
|
||||
| [allow.txt](allow.txt) | Add words to the dictionary | one word per line (only letters and `'`s allowed) | [allow](https://github.com/check-spelling/check-spelling/wiki/Configuration#allow) |
|
||||
| [reject.txt](reject.txt) | Remove words from the dictionary (after allow) | grep pattern matching whole dictionary words | [reject](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-reject) |
|
||||
| [excludes.txt](excludes.txt) | Files to ignore entirely | perl regular expression | [excludes](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-excludes) |
|
||||
| [only.txt](only.txt) | Only check matching files (applied after excludes) | perl regular expression | [only](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-only) |
|
||||
| [patterns.txt](patterns.txt) | Patterns to ignore from checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns) |
|
||||
| [candidate.patterns](candidate.patterns) | Patterns that might be worth adding to [patterns.txt](patterns.txt) | perl regular expression with optional comment block introductions (all matches will be suggested) | [candidates](https://github.com/check-spelling/check-spelling/wiki/Feature:-Suggest-patterns) |
|
||||
| [line_forbidden.patterns](line_forbidden.patterns) | Patterns to flag in checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns) |
|
||||
| [expect.txt](expect.txt) | Expected words that aren't in the dictionary | one word per line (sorted, alphabetically) | [expect](https://github.com/check-spelling/check-spelling/wiki/Configuration#expect) |
|
||||
| [advice.md](advice.md) | Supplement for GitHub comment when unrecognized words are found | GitHub Markdown | [advice](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice) |
|
||||
|
||||
Note: you can replace any of these files with a directory by the same name (minus the suffix)
|
||||
and then include multiple files inside that directory (with that suffix) to merge multiple files together.
|
||||
24
.github/actions/docs-spelling/advice.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<!-- See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice --> <!-- markdownlint-disable MD033 MD041 -->
|
||||
<details>
|
||||
<summary>If the flagged items are :exploding_head: false positives</summary>
|
||||
|
||||
If items relate to a ...
|
||||
|
||||
- binary file (or some other file you wouldn't want to check at all).
|
||||
|
||||
Please add a file path to the `excludes.txt` file matching the containing file.
|
||||
|
||||
File paths are Perl 5 Regular Expressions - you can [test](https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your files.
|
||||
|
||||
`^` refers to the file's path from the root of the repository, so `^README\.md$` would exclude [README.md](../tree/HEAD/README.md) (on whichever branch you're using).
|
||||
|
||||
- well-formed pattern.
|
||||
|
||||
If you can write a [pattern](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns) that would match it,
|
||||
try adding it to the `patterns.txt` file.
|
||||
|
||||
Patterns are Perl 5 Regular Expressions - you can [test](https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your lines.
|
||||
|
||||
Note that patterns can't match multiline strings.
|
||||
|
||||
</details>
|
||||
8
.github/actions/docs-spelling/allow/contributers.txt
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
trevdor
|
||||
Farlow
|
||||
Matiss
|
||||
Aboltins
|
||||
jlongster
|
||||
howell
|
||||
evequefou
|
||||
Fiddaman
|
||||
144
.github/actions/docs-spelling/allow/keywords.txt
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
ABANCA
|
||||
actualbudget
|
||||
addtransactions
|
||||
Akahu
|
||||
AMZN
|
||||
Andelskassen
|
||||
AQL
|
||||
Authelia
|
||||
autocompletes
|
||||
Blix
|
||||
bnp
|
||||
BSCHESMM
|
||||
BTC
|
||||
CAGLESMM
|
||||
Caju
|
||||
caniuse
|
||||
Cardless
|
||||
CAROOT
|
||||
categorygroup
|
||||
Cembra
|
||||
Certbot
|
||||
CLI
|
||||
clickable
|
||||
clsx
|
||||
codemirror
|
||||
Coinbase
|
||||
commandlet
|
||||
Coverflex
|
||||
Crd
|
||||
crdt
|
||||
creditcards
|
||||
crowdsourced
|
||||
debian
|
||||
dedupes
|
||||
deleteaccount
|
||||
DKB
|
||||
dmg
|
||||
easybank
|
||||
Edenred
|
||||
Coverfelx
|
||||
emojis
|
||||
emoji
|
||||
escodegen
|
||||
EUR
|
||||
expando
|
||||
Firefox
|
||||
flyctl
|
||||
Formik
|
||||
Fortuneo
|
||||
gebabebb
|
||||
GEBABEBB
|
||||
Greenshot
|
||||
HSA
|
||||
htpasswd
|
||||
IBANs
|
||||
iex
|
||||
importtransactions
|
||||
ING
|
||||
invokable
|
||||
iwr
|
||||
jointaccounts
|
||||
jwl
|
||||
KBC
|
||||
kcab
|
||||
keyout
|
||||
KREDBEBB
|
||||
Kroger
|
||||
kubectl
|
||||
kubernetes
|
||||
ldaplogin
|
||||
letsencrypt
|
||||
libofx
|
||||
linting
|
||||
Linuxes
|
||||
linuxsvg
|
||||
lleskassen
|
||||
lte
|
||||
mac
|
||||
macsvg
|
||||
Mariushosting
|
||||
minimalistic
|
||||
monkeypatch
|
||||
Monobank
|
||||
Morrisons
|
||||
NAIAGB
|
||||
NDEADKKK
|
||||
Netflix
|
||||
netlify
|
||||
Nordea
|
||||
NORDEA
|
||||
nordigen
|
||||
notlike
|
||||
NRNBGB
|
||||
nynab
|
||||
offbudget
|
||||
ofx
|
||||
OFX
|
||||
oneof
|
||||
payeerule
|
||||
pikaday
|
||||
pikapods
|
||||
playsinline
|
||||
portalization
|
||||
Postgresql
|
||||
protobuf
|
||||
publix
|
||||
QFX
|
||||
QIF
|
||||
Quicken
|
||||
returnsandreimbursements
|
||||
Rezip
|
||||
roadmap
|
||||
RUpdate
|
||||
sankey
|
||||
SANTANDER
|
||||
screenshots
|
||||
SEB
|
||||
subfolders
|
||||
subreaper
|
||||
subtransaction
|
||||
subtransactions
|
||||
Suisse
|
||||
Sztup
|
||||
tini
|
||||
traefik
|
||||
Trafico
|
||||
Trumf
|
||||
Upstash
|
||||
useb
|
||||
usernames
|
||||
valign
|
||||
Venmo
|
||||
Weblate
|
||||
winsvg
|
||||
WSL
|
||||
Xxxxx
|
||||
ynab
|
||||
Ynab
|
||||
YNAB
|
||||
ZKB
|
||||
Zsolt
|
||||
IDBy
|
||||
isapprox
|
||||
isbetween
|
||||
76
.github/actions/docs-spelling/excludes.txt
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-excludes
|
||||
(?:^|/)(?i)COPYRIGHT
|
||||
(?:^|/)(?i)LICEN[CS]E
|
||||
(?:^|/)3rdparty/
|
||||
(?:^|/)go\.sum$
|
||||
(?:^|/)package(?:-lock|)\.json$
|
||||
(?:^|/)pyproject.toml
|
||||
(?:^|/)requirements(?:-dev|-doc|-test|)\.txt$
|
||||
(?:^|/)vendor/
|
||||
ignore$
|
||||
\.a$
|
||||
\.ai$
|
||||
\.avi$
|
||||
\.bmp$
|
||||
\.bz2$
|
||||
\.class$
|
||||
\.coveragerc$
|
||||
\.crt$
|
||||
\.css$
|
||||
\.dll$
|
||||
\.docx?$
|
||||
\.drawio$
|
||||
\.DS_Store$
|
||||
\.eot$
|
||||
\.exe$
|
||||
\.gif$
|
||||
\.git-blame-ignore-revs$
|
||||
\.gitattributes$
|
||||
\.graffle$
|
||||
\.gz$
|
||||
\.icns$
|
||||
\.ico$
|
||||
\.jar$
|
||||
\.jks$
|
||||
\.jpe?g$
|
||||
\.key$
|
||||
\.lib$
|
||||
\.lock$
|
||||
\.map$
|
||||
\.min\..
|
||||
\.mod$
|
||||
\.mp[34]$
|
||||
\.o$
|
||||
\.ocf$
|
||||
\.otf$
|
||||
\.pdf$
|
||||
\.pem$
|
||||
\.png$
|
||||
\.psd$
|
||||
\.pyc$
|
||||
\.pylintrc$
|
||||
\.s$
|
||||
\.svgz?$
|
||||
\.tar$
|
||||
\.tiff?$
|
||||
\.ttf$
|
||||
\.wav$
|
||||
\.webm$
|
||||
\.webp$
|
||||
\.woff2?$
|
||||
\.xlsx?$
|
||||
\.zip$
|
||||
^\.github/actions/spelling/
|
||||
^\.github/ISSUE_TEMPLATE/
|
||||
^\Q.github/workflows/spelling.yml\E$
|
||||
^\.yarn/
|
||||
^\Qnode_modules/\E$
|
||||
^\Qsrc/\E$
|
||||
^\Qstatic/\E$
|
||||
^\Q.github/\E$
|
||||
(?:^|/)package(?:-lock|)\.json$
|
||||
(?:^|/)yarn\.lock$
|
||||
(?:^|/)(?i)docusaurus.config.js
|
||||
(?:^|/)(?i)README.md
|
||||
^\static/
|
||||
\.tsx$
|
||||
152
.github/actions/docs-spelling/expect.txt
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
Abanca
|
||||
ABNAMRO
|
||||
ABNANL
|
||||
Activo
|
||||
AESUDEF
|
||||
ALZEY
|
||||
Anglais
|
||||
aql
|
||||
AUR
|
||||
Authentik
|
||||
BANKA
|
||||
BANKINTER
|
||||
BAWAATWW
|
||||
Belfius
|
||||
Biedenkopf
|
||||
BIGBPLPW
|
||||
Bizum
|
||||
BKBKESMM
|
||||
BOFIIE
|
||||
Bourso
|
||||
Boursobank
|
||||
Boursorama
|
||||
BPER
|
||||
BPMOIT
|
||||
brexplpw
|
||||
BYLADEM
|
||||
Caddyfile
|
||||
CAGLPTPL
|
||||
Caixa
|
||||
CAMT
|
||||
cashflow
|
||||
Cetelem
|
||||
cimode
|
||||
Citi
|
||||
Citibank
|
||||
Cloudflare
|
||||
CMCIFRPAXXX
|
||||
COBADEFF
|
||||
CODEOWNERS
|
||||
commerzbank
|
||||
Copiar
|
||||
CREGBEBB
|
||||
crt
|
||||
Danske
|
||||
datadir
|
||||
Depositos
|
||||
DIREKT
|
||||
Dockerfiles
|
||||
Dominguez
|
||||
DUSSDEDDXXX
|
||||
DUSSELDORF
|
||||
ENTERCARD
|
||||
Entra
|
||||
EUA
|
||||
Eurocard
|
||||
fidd
|
||||
Fineco
|
||||
Finicity
|
||||
Fintro
|
||||
Finverse
|
||||
Flathub
|
||||
FORTUNEO
|
||||
FTNOFRP
|
||||
Gemeinschaftsbank
|
||||
Geral
|
||||
gernes
|
||||
Globecard
|
||||
GLS
|
||||
gocardless
|
||||
Grafana
|
||||
HABAL
|
||||
Hampel
|
||||
HELADEF
|
||||
INDUSTRIEL
|
||||
INGBPLPW
|
||||
Ingo
|
||||
INR
|
||||
Intesa
|
||||
INVSTMTMSGSRS
|
||||
ISYBANK
|
||||
ITBBITMM
|
||||
jfdoming
|
||||
JMD
|
||||
KBCBE
|
||||
Keycloak
|
||||
Khurozov
|
||||
KORT
|
||||
Kreditbank
|
||||
lage
|
||||
LHV
|
||||
LHVBEE
|
||||
LKR
|
||||
mbank
|
||||
mdc
|
||||
modals
|
||||
Moldovan
|
||||
murmurhash
|
||||
nginx
|
||||
OIDC
|
||||
overbudgeted
|
||||
overbudgeting
|
||||
Paribas
|
||||
passwordless
|
||||
pluggyai
|
||||
Poste
|
||||
PPABPLPK
|
||||
prefs
|
||||
Primoco
|
||||
Priotecs
|
||||
proactively
|
||||
pwa
|
||||
Qatari
|
||||
QNTOFRP
|
||||
QONTO
|
||||
Raiffeisen
|
||||
revolut
|
||||
RIED
|
||||
RSchedule
|
||||
RSD
|
||||
SEK
|
||||
simplefin
|
||||
SKHSFI
|
||||
Sparkasse
|
||||
SPK
|
||||
sseldorf
|
||||
SSK
|
||||
Stadtsparkasse
|
||||
statestore
|
||||
SUBASKBX
|
||||
SVGR
|
||||
swc
|
||||
SWEDBANK
|
||||
SWEDNOKK
|
||||
Synology
|
||||
systemctl
|
||||
tada
|
||||
taskbar
|
||||
templating
|
||||
THB
|
||||
touchscreen
|
||||
triaging
|
||||
UAH
|
||||
ubuntu
|
||||
userinfo
|
||||
Userscripts
|
||||
UZS
|
||||
vrt
|
||||
VUB
|
||||
websecure
|
||||
Widiba
|
||||
WOR
|
||||
youngcw
|
||||
62
.github/actions/docs-spelling/line_forbidden.patterns
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# reject `m_data` as there's a certain OS which has evil defines that break things if it's used elsewhere
|
||||
# \bm_data\b
|
||||
|
||||
# If you have a framework that uses `it()` for testing and `fit()` for debugging a specific test,
|
||||
# you might not want to check in code where you were debugging w/ `fit()`, in which case, you might want
|
||||
# to use this:
|
||||
#\bfit\(
|
||||
|
||||
# s.b. GitHub
|
||||
#\bGithub\b
|
||||
|
||||
# s.b. GitLab
|
||||
\bGitlab\b
|
||||
|
||||
# s.b. JavaScript
|
||||
\bJavascript\b
|
||||
|
||||
# s.b. Microsoft
|
||||
\bMicroSoft\b
|
||||
|
||||
# s.b. another
|
||||
\ban[- ]other\b
|
||||
|
||||
# s.b. greater than
|
||||
\bgreater then\b
|
||||
|
||||
# s.b. into
|
||||
#\sin to\s
|
||||
|
||||
# s.b. opt-in
|
||||
\sopt in\s
|
||||
|
||||
# s.b. less than
|
||||
\bless then\b
|
||||
|
||||
# s.b. otherwise
|
||||
\bother[- ]wise\b
|
||||
|
||||
# s.b. nonexistent
|
||||
\bnon existing\b
|
||||
\b[Nn]o[nt][- ]existent\b
|
||||
|
||||
# s.b. preexisting
|
||||
[Pp]re[- ]existing
|
||||
|
||||
# s.b. preempt
|
||||
[Pp]re[- ]empt\b
|
||||
|
||||
# s.b. preemptively
|
||||
[Pp]re[- ]emptively
|
||||
|
||||
# s.b. reentrancy
|
||||
[Rr]e[- ]entrancy
|
||||
|
||||
# s.b. reentrant
|
||||
[Rr]e[- ]entrant
|
||||
|
||||
# s.b. workaround(s)
|
||||
\bwork[- ]arounds?\b
|
||||
|
||||
# Reject duplicate words
|
||||
\s([A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})\s\g{-1}\s
|
||||
3
.github/actions/docs-spelling/only.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Only check files in the packages/docs directory
|
||||
^packages/docs/
|
||||
|
||||
81
.github/actions/docs-spelling/patterns.txt
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns
|
||||
|
||||
# Questionably acceptable forms of `in to`
|
||||
# Personally, I prefer `log into`, but people object
|
||||
# https://www.tprteaching.com/log-into-log-in-to-login/
|
||||
\b[Ll]og in to\b
|
||||
|
||||
# acceptable duplicates
|
||||
# ls directory listings
|
||||
[-bcdlpsw](?:[-r][-w][-Ssx]){3}\s+\d+\s+\S+\s+\S+\s+\d+\s+
|
||||
# C types and repeated CSS values
|
||||
\s(center|div|inherit|long|LONG|none|normal|solid|thin|transparent|very)(?: \g{-1})+\s
|
||||
# go templates
|
||||
\s(\w+)\s+\g{-1}\s+\`(?:graphql|json|yaml):
|
||||
# javadoc / .net
|
||||
(?:[\\@](?:groupname|param)|(?:public|private)(?:\s+static|\s+readonly)*)\s+(\w+)\s+\g{-1}\s
|
||||
|
||||
# Commit message -- Signed-off-by and friends
|
||||
^\s*(?:(?:Based-on-patch|Co-authored|Helped|Mentored|Reported|Reviewed|Signed-off)-by|Thanks-to): (?:[^<]*<[^>]*>|[^<]*)\s*$
|
||||
|
||||
# Autogenerated revert commit message
|
||||
^This reverts commit [0-9a-f]{40}\.$
|
||||
|
||||
# ignore long runs of a single character:
|
||||
\b([A-Za-z])\g{-1}{3,}\b
|
||||
|
||||
# Automatically suggested patterns
|
||||
# hit-count: 1255 file-count: 51
|
||||
# https/http/file urls
|
||||
(?:\b(?:https?|ftp|file)://)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]
|
||||
|
||||
# hit-count: 1174 file-count: 33
|
||||
# GitHub SHAs (markdown)
|
||||
(?:\[`?[0-9a-f]+`?\]\(https:/|)/(?:www\.|)github\.com(?:/[^/\s"]+){2,}(?:/[^/\s")]+)(?:[0-9a-f]+(?:[-0-9a-zA-Z/#.]*|)\b|)
|
||||
|
||||
# hit-count: 6 file-count: 4
|
||||
# version suffix <word>v#
|
||||
(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_]))
|
||||
|
||||
# hit-count: 6 file-count: 2
|
||||
# URL escaped characters
|
||||
\%[0-9A-F][A-F]
|
||||
|
||||
# hit-count: 5 file-count: 4
|
||||
# hex runs
|
||||
\b[0-9a-fA-F]{16,}\b
|
||||
|
||||
# hit-count: 4 file-count: 2
|
||||
# uuid:
|
||||
\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b
|
||||
|
||||
# hit-count: 3 file-count: 2
|
||||
# discord
|
||||
/discord(?:app\.com|\.gg)/(?:invite/)?[a-zA-Z0-9]{7,}
|
||||
|
||||
# hit-count: 2 file-count: 2
|
||||
# Contributor
|
||||
\[[^\]]+\]\(https://github\.com/[^/\s"]+\)
|
||||
@[^$\W]*-?\w+
|
||||
|
||||
# hit-count: 1 file-count: 1
|
||||
# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there
|
||||
# YouTube url
|
||||
\b(?:(?:www\.|)youtube\.com|youtu.be)/(?:channel/|embed/|user/|playlist\?list=|watch\?v=|v/|)[-a-zA-Z0-9?&=_%]*
|
||||
|
||||
# hit-count: 1 file-count: 1
|
||||
# Google Fonts
|
||||
\bfonts\.(?:googleapis|gstatic)\.com/[-/?=:;+&0-9a-zA-Z]*
|
||||
|
||||
# hit-count: 1 file-count: 1
|
||||
# hex digits including css/html color classes:
|
||||
(?:[\\0][xX]|\\u|[uU]\+|#x?|\%23)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|u\d+)\b
|
||||
|
||||
# docusaurus image paths, URLs
|
||||
[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?
|
||||
|
||||
# eliminate words like [`nvm`] or [`asdf`] or [heidiSQL] without backquotes
|
||||
\[.+?]
|
||||
|
||||
# allowlist specific non-English words with non-ASCII characters
|
||||
\b(Länsförsäkringar|München|Złoty)\b
|
||||
10
.github/actions/docs-spelling/reject.txt
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
^attache$
|
||||
benefitting
|
||||
occurences?
|
||||
^dependan.*
|
||||
^oer$
|
||||
Sorce
|
||||
^[Ss]pae.*
|
||||
^untill$
|
||||
^untilling$
|
||||
^wether.*
|
||||
8
.github/actions/setup/action.yml
vendored
@@ -15,7 +15,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install yarn
|
||||
@@ -27,7 +27,7 @@ runs:
|
||||
run: echo "version=$(node -v)" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
id: cache
|
||||
with:
|
||||
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
|
||||
@@ -36,7 +36,7 @@ runs:
|
||||
run: mkdir -p ${{ format('{0}/.lage', inputs.working-directory) }}
|
||||
shell: bash
|
||||
- name: Cache Lage
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ format('{0}/.lage', inputs.working-directory) }}
|
||||
key: lage-${{ runner.os }}-${{ github.sha }}
|
||||
@@ -48,7 +48,7 @@ runs:
|
||||
shell: bash
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
- name: Download translations
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
repository: actualbudget/translations
|
||||
path: ${{ inputs.working-directory }}/packages/desktop-client/locale
|
||||
|
||||
260
.github/scripts/count-points.mjs
vendored
@@ -4,43 +4,32 @@ import pLimit from 'p-limit';
|
||||
|
||||
const limit = pLimit(50);
|
||||
|
||||
/** Repository-specific configuration for points calculation */
|
||||
const REPOSITORY_CONFIG = new Map([
|
||||
[
|
||||
'actual',
|
||||
{
|
||||
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
|
||||
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
|
||||
POINTS_PER_RELEASE_PR: 0,
|
||||
PR_REVIEW_POINT_TIERS: [
|
||||
{ minChanges: 500, points: 8 },
|
||||
{ minChanges: 100, points: 6 },
|
||||
{ minChanges: 10, points: 2 },
|
||||
{ minChanges: 0, points: 1 },
|
||||
],
|
||||
EXCLUDED_FILES: [
|
||||
'yarn.lock',
|
||||
'.yarn/**/*',
|
||||
'packages/component-library/src/icons/**/*',
|
||||
'release-notes/**/*',
|
||||
],
|
||||
},
|
||||
const CONFIG = {
|
||||
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
|
||||
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
|
||||
POINTS_PER_RELEASE_PR: 4, // Awarded to whoever merges the release PR
|
||||
// Point tiers for code changes (non-docs)
|
||||
CODE_PR_REVIEW_POINT_TIERS: [
|
||||
{ minChanges: 500, points: 8 },
|
||||
{ minChanges: 100, points: 6 },
|
||||
{ minChanges: 10, points: 2 },
|
||||
{ minChanges: 0, points: 1 },
|
||||
],
|
||||
[
|
||||
'docs',
|
||||
{
|
||||
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
|
||||
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
|
||||
POINTS_PER_RELEASE_PR: 4,
|
||||
PR_REVIEW_POINT_TIERS: [
|
||||
{ minChanges: 2000, points: 6 },
|
||||
{ minChanges: 200, points: 4 },
|
||||
{ minChanges: 0, points: 2 },
|
||||
],
|
||||
EXCLUDED_FILES: ['yarn.lock', '.yarn/**/*'],
|
||||
},
|
||||
// Point tiers for docs changes (packages/docs/**)
|
||||
DOCS_PR_REVIEW_POINT_TIERS: [
|
||||
{ minChanges: 2000, points: 6 },
|
||||
{ minChanges: 200, points: 4 },
|
||||
{ minChanges: 0, points: 2 },
|
||||
],
|
||||
]);
|
||||
EXCLUDED_FILES: [
|
||||
'yarn.lock',
|
||||
'.yarn/**/*',
|
||||
'packages/component-library/src/icons/**/*',
|
||||
'release-notes/**/*',
|
||||
'upcoming-release-notes/**/*',
|
||||
],
|
||||
DOCS_FILES_PATTERN: 'packages/docs/**/*',
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the start and end dates for the last month.
|
||||
@@ -76,15 +65,14 @@ function getLastMonthDates() {
|
||||
/**
|
||||
* Used for calculating the monthly points each core contributor has earned.
|
||||
* These are used for payouts depending.
|
||||
* @param {string} repo - The repository to analyze ('actual' or 'docs')
|
||||
* @returns {number} The total points earned for the repository
|
||||
* @returns {Map} A map of contributor logins to their total points earned
|
||||
*/
|
||||
async function countContributorPoints(repo) {
|
||||
async function countContributorPoints() {
|
||||
const octokit = new Octokit({
|
||||
auth: process.env.GITHUB_TOKEN,
|
||||
});
|
||||
const owner = 'actualbudget';
|
||||
const config = REPOSITORY_CONFIG.get(repo);
|
||||
const repo = 'actual';
|
||||
|
||||
const { since, until } = getLastMonthDates();
|
||||
|
||||
@@ -99,7 +87,8 @@ async function countContributorPoints(repo) {
|
||||
Array.from(orgMemberLogins).map(login => [
|
||||
login,
|
||||
{
|
||||
reviews: [], // Will store objects with PR number and points
|
||||
codeReviews: [], // Will store objects with PR number and points for main repo changes
|
||||
docsReviews: [], // Will store objects with PR number and points for docs changes
|
||||
labelRemovals: [],
|
||||
issueClosings: [],
|
||||
points: 0,
|
||||
@@ -156,48 +145,91 @@ async function countContributorPoints(repo) {
|
||||
),
|
||||
]);
|
||||
|
||||
const totalChanges = modifiedFiles
|
||||
.filter(
|
||||
file =>
|
||||
!config.EXCLUDED_FILES.some(pattern =>
|
||||
minimatch(file.filename, pattern),
|
||||
),
|
||||
)
|
||||
.reduce((sum, file) => sum + file.additions + file.deletions, 0);
|
||||
const filteredFiles = modifiedFiles.filter(
|
||||
file =>
|
||||
!CONFIG.EXCLUDED_FILES.some(pattern =>
|
||||
minimatch(file.filename, pattern, { dot: true }),
|
||||
),
|
||||
);
|
||||
|
||||
const docsFiles = filteredFiles.filter(file =>
|
||||
minimatch(file.filename, CONFIG.DOCS_FILES_PATTERN, { dot: true }),
|
||||
);
|
||||
const codeFiles = filteredFiles.filter(
|
||||
file =>
|
||||
!minimatch(file.filename, CONFIG.DOCS_FILES_PATTERN, { dot: true }),
|
||||
);
|
||||
|
||||
const docsChanges = docsFiles.reduce(
|
||||
(sum, file) => sum + file.additions + file.deletions,
|
||||
0,
|
||||
);
|
||||
const codeChanges = codeFiles.reduce(
|
||||
(sum, file) => sum + file.additions + file.deletions,
|
||||
0,
|
||||
);
|
||||
|
||||
const docsPoints =
|
||||
docsChanges > 0
|
||||
? (CONFIG.DOCS_PR_REVIEW_POINT_TIERS.find(
|
||||
t => docsChanges >= t.minChanges,
|
||||
)?.points ?? 0)
|
||||
: 0;
|
||||
const codePoints =
|
||||
codeChanges > 0 || docsChanges === 0
|
||||
? (CONFIG.CODE_PR_REVIEW_POINT_TIERS.find(
|
||||
t => codeChanges >= t.minChanges,
|
||||
)?.points ?? 0)
|
||||
: 0;
|
||||
|
||||
const isReleasePR = pr.title.match(/🔖.*\d+\.\d+\.\d+/);
|
||||
const prPoints =
|
||||
config.PR_REVIEW_POINT_TIERS.find(t => totalChanges >= t.minChanges)
|
||||
?.points ?? 0;
|
||||
|
||||
if (isReleasePR) {
|
||||
if (stats.has(pr.user.login)) {
|
||||
const creatorStats = stats.get(pr.user.login);
|
||||
creatorStats.reviews.push({
|
||||
// release PRs are created by the github-actions bot so we attribute points to the merger
|
||||
const { data: prDetails } = await octokit.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
});
|
||||
|
||||
if (prDetails.merged_by && stats.has(prDetails.merged_by.login)) {
|
||||
const mergerStats = stats.get(prDetails.merged_by.login);
|
||||
mergerStats.codeReviews.push({
|
||||
pr: pr.number.toString(),
|
||||
points: config.POINTS_PER_RELEASE_PR,
|
||||
isReleaseCreator: true,
|
||||
points: CONFIG.POINTS_PER_RELEASE_PR,
|
||||
isReleaseMerger: true,
|
||||
});
|
||||
creatorStats.points += config.POINTS_PER_RELEASE_PR;
|
||||
mergerStats.points += CONFIG.POINTS_PER_RELEASE_PR;
|
||||
}
|
||||
} else {
|
||||
const uniqueReviewers = new Set();
|
||||
reviews.data
|
||||
.filter(
|
||||
review =>
|
||||
stats.has(review.user?.login) &&
|
||||
review.state === 'APPROVED' &&
|
||||
!uniqueReviewers.has(review.user?.login),
|
||||
)
|
||||
.forEach(({ user: { login: reviewer } }) => {
|
||||
reviews.data.forEach(review => {
|
||||
if (
|
||||
review.state === 'APPROVED' &&
|
||||
stats.has(review.user?.login) &&
|
||||
!uniqueReviewers.has(review.user?.login)
|
||||
) {
|
||||
const reviewer = review.user.login;
|
||||
uniqueReviewers.add(reviewer);
|
||||
const userStats = stats.get(reviewer);
|
||||
userStats.reviews.push({
|
||||
pr: pr.number.toString(),
|
||||
points: prPoints,
|
||||
});
|
||||
userStats.points += prPoints;
|
||||
});
|
||||
|
||||
if (docsPoints > 0) {
|
||||
userStats.docsReviews.push({
|
||||
pr: pr.number.toString(),
|
||||
points: docsPoints,
|
||||
});
|
||||
userStats.points += docsPoints;
|
||||
}
|
||||
|
||||
if (codePoints > 0) {
|
||||
userStats.codeReviews.push({
|
||||
pr: pr.number.toString(),
|
||||
points: codePoints,
|
||||
});
|
||||
userStats.points += codePoints;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
@@ -241,7 +273,7 @@ async function countContributorPoints(repo) {
|
||||
const remover = event.actor.login;
|
||||
const userStats = stats.get(remover);
|
||||
userStats.labelRemovals.push(issue.number.toString());
|
||||
userStats.points += config.POINTS_PER_ISSUE_TRIAGE_ACTION;
|
||||
userStats.points += CONFIG.POINTS_PER_ISSUE_TRIAGE_ACTION;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -251,7 +283,7 @@ async function countContributorPoints(repo) {
|
||||
const closer = event.actor.login;
|
||||
const userStats = stats.get(closer);
|
||||
userStats.issueClosings.push(issue.number.toString());
|
||||
userStats.points += config.POINTS_PER_ISSUE_CLOSING_ACTION;
|
||||
userStats.points += CONFIG.POINTS_PER_ISSUE_CLOSING_ACTION;
|
||||
}
|
||||
});
|
||||
}),
|
||||
@@ -260,27 +292,39 @@ async function countContributorPoints(repo) {
|
||||
|
||||
// Print all statistics
|
||||
printStats(
|
||||
`PR Review Statistics (${repo})`,
|
||||
stats => stats.reviews.length,
|
||||
'Code Review Statistics',
|
||||
stats => stats.codeReviews.length,
|
||||
(user, count) =>
|
||||
`${user}: ${count} (PRs: ${stats
|
||||
.get(user)
|
||||
.reviews.map(r => {
|
||||
if (r.isReleaseCreator) {
|
||||
return `#${r.pr} (${r.points}pts - Release Creator)`;
|
||||
.codeReviews.map(r => {
|
||||
if (r.isReleaseMerger) {
|
||||
return `#${r.pr} (${r.points}pts - Release Merger)`;
|
||||
}
|
||||
return `#${r.pr} (${r.points}pts)`;
|
||||
})
|
||||
.join(', ')})`,
|
||||
);
|
||||
|
||||
printStats(
|
||||
`"Needs Triage" Label Removal Statistics (${repo})`,
|
||||
'Docs Review Statistics',
|
||||
stats => stats.docsReviews.length,
|
||||
(user, count) =>
|
||||
`${user}: ${count} (PRs: ${stats
|
||||
.get(user)
|
||||
.docsReviews.map(r => `#${r.pr} (${r.points}pts)`)
|
||||
.join(', ')})`,
|
||||
);
|
||||
|
||||
printStats(
|
||||
'"Needs Triage" Label Removal Statistics',
|
||||
stats => stats.labelRemovals.length,
|
||||
(user, count) =>
|
||||
`${user}: ${count} (Issues: ${stats.get(user).labelRemovals.join(', ')})`,
|
||||
);
|
||||
|
||||
printStats(
|
||||
`Issue Closing Statistics (${repo})`,
|
||||
'Issue Closing Statistics',
|
||||
stats => stats.issueClosings.length,
|
||||
(user, count) =>
|
||||
`${user}: ${count} (Issues: ${stats.get(user).issueClosings.join(', ')})`,
|
||||
@@ -288,7 +332,7 @@ async function countContributorPoints(repo) {
|
||||
|
||||
// Print points summary
|
||||
printStats(
|
||||
`Points Summary (${repo})`,
|
||||
'Points Summary',
|
||||
stats => stats.points,
|
||||
(user, userPoints) => `${user}: ${userPoints}`,
|
||||
);
|
||||
@@ -298,7 +342,7 @@ async function countContributorPoints(repo) {
|
||||
(sum, userStats) => sum + userStats.points,
|
||||
0,
|
||||
);
|
||||
console.log(`\nTotal points earned for ${repo}: ${totalPoints}`);
|
||||
console.log(`\nTotal points earned: ${totalPoints}`);
|
||||
|
||||
// Return the points
|
||||
return new Map(
|
||||
@@ -309,55 +353,5 @@ async function countContributorPoints(repo) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the points for both repositories and print cumulative results
|
||||
*/
|
||||
async function calculateCumulativePoints() {
|
||||
// Get stats for each repository
|
||||
const repoPointsResults = await Promise.all(
|
||||
Array.from(REPOSITORY_CONFIG.keys()).map(countContributorPoints),
|
||||
);
|
||||
|
||||
// Calculate cumulative stats
|
||||
const cumulativeStats = new Map(repoPointsResults[0]);
|
||||
|
||||
// Combine stats from all repositories
|
||||
for (let i = 1; i < repoPointsResults.length; i++) {
|
||||
for (const [login, points] of repoPointsResults[i].entries()) {
|
||||
if (!cumulativeStats.has(login)) {
|
||||
cumulativeStats.set(login, 0);
|
||||
}
|
||||
|
||||
cumulativeStats.set(login, cumulativeStats.get(login) + points);
|
||||
}
|
||||
}
|
||||
|
||||
// Print cumulative statistics
|
||||
console.log('\n\nCUMULATIVE STATISTICS ACROSS ALL REPOSITORIES');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
console.log('\nCumulative Points Summary:');
|
||||
console.log('='.repeat('Cumulative Points Summary'.length + 1));
|
||||
|
||||
const entries = Array.from(cumulativeStats.entries())
|
||||
.filter(([, count]) => count > 0)
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log('No cumulative points summary found.');
|
||||
} else {
|
||||
entries.forEach(([user, points]) => {
|
||||
console.log(`${user}: ${points}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate and print total cumulative points
|
||||
const totalCumulativePoints = Array.from(cumulativeStats.values()).reduce(
|
||||
(sum, points) => sum + points,
|
||||
0,
|
||||
);
|
||||
console.log('\nTotal cumulative points earned: ' + totalCumulativePoints);
|
||||
}
|
||||
|
||||
// Run the calculations
|
||||
calculateCumulativePoints().catch(console.error);
|
||||
countContributorPoints().catch(console.error);
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
|
||||
2
.github/workflows/autofix.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
autofix:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Format code
|
||||
|
||||
26
.github/workflows/build.yml
vendored
@@ -12,6 +12,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -21,7 +22,7 @@ jobs:
|
||||
api:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -30,16 +31,23 @@ jobs:
|
||||
run: cd packages/api && yarn build
|
||||
- name: Create package tgz
|
||||
run: cd packages/api && yarn pack && mv package.tgz actual-api.tgz
|
||||
- name: Prepare bundle stats artifact
|
||||
run: cp packages/api/app/stats.json api-stats.json
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: actual-api
|
||||
path: packages/api/actual-api.tgz
|
||||
- name: Upload API bundle stats
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: api-build-stats
|
||||
path: api-stats.json
|
||||
|
||||
crdt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -49,7 +57,7 @@ jobs:
|
||||
- name: Create package tgz
|
||||
run: cd packages/crdt && yarn pack && mv package.tgz actual-crdt.tgz
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: actual-crdt
|
||||
path: packages/crdt/actual-crdt.tgz
|
||||
@@ -57,18 +65,18 @@ jobs:
|
||||
web:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: yarn build:browser
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: actual-web
|
||||
path: packages/desktop-client/build
|
||||
- name: Upload Build Stats
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: build-stats
|
||||
path: packages/desktop-client/build-stats
|
||||
@@ -76,7 +84,7 @@ jobs:
|
||||
server:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -84,7 +92,7 @@ jobs:
|
||||
- name: Build Server
|
||||
run: yarn workspace @actual-app/sync-server build
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: sync-server
|
||||
path: packages/sync-server/build
|
||||
|
||||
13
.github/workflows/check.yml
vendored
@@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -14,7 +15,7 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -24,7 +25,7 @@ jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -34,7 +35,7 @@ jobs:
|
||||
validate-cli:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -46,7 +47,7 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -58,8 +59,8 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Check migrations
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
||||
2
.github/workflows/count-points.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Count points
|
||||
|
||||
16
.github/workflows/docker-edge.yml
vendored
@@ -36,17 +36,17 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu, alpine]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||
with:
|
||||
# Push to both Docker Hub and Github Container Registry
|
||||
images: ${{ env.IMAGES }}
|
||||
@@ -54,14 +54,14 @@ jobs:
|
||||
tags: ${{ env.TAGS }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
if: github.event_name != 'pull_request' && !github.event.repository.fork
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
run: yarn build:server
|
||||
|
||||
- name: Build image for testing
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
# This will use the cache from the earlier build step and not rebuild the image
|
||||
# https://docs.docker.com/build/ci/github-actions/test-before-push/
|
||||
- name: Build and push images
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
18
.github/workflows/docker-release.yml
vendored
@@ -28,17 +28,17 @@ jobs:
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||
with:
|
||||
# Push to both Docker Hub and Github Container Registry
|
||||
images: ${{ env.IMAGES }}
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
- name: Docker meta for Alpine image
|
||||
id: alpine-meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||
with:
|
||||
images: ${{ env.IMAGES }}
|
||||
# Automatically update :latest
|
||||
@@ -58,13 +58,13 @@ jobs:
|
||||
tags: ${{ env.TAGS }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
run: yarn build:server
|
||||
|
||||
- name: Build and push ubuntu image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
- name: Build and push alpine image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
164
.github/workflows/docs-spelling.yml
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
name: Check Spelling (Docs)
|
||||
|
||||
# Comment management is handled through a secondary job, for details see:
|
||||
# https://github.com/check-spelling/check-spelling/wiki/Feature%3A-Restricted-Permissions
|
||||
#
|
||||
# `jobs.comment-push` runs when a push is made to a repository and the `jobs.spelling` job needs to make a comment
|
||||
# (in odd cases, it might actually run just to collapse a comment, but that's fairly rare)
|
||||
# it needs `contents: write` in order to add a comment.
|
||||
#
|
||||
# `jobs.comment-pr` runs when a pull_request is made to a repository and the `jobs.spelling` job needs to make a comment
|
||||
# or collapse a comment (in the case where it had previously made a comment and now no longer needs to show a comment)
|
||||
# it needs `pull-requests: write` in order to manipulate those comments.
|
||||
|
||||
# Updating pull request branches is managed via comment handling.
|
||||
# For details, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-expect-list
|
||||
#
|
||||
# These elements work together to make it happen:
|
||||
#
|
||||
# `on.issue_comment`
|
||||
# This event listens to comments by users asking to update the metadata.
|
||||
#
|
||||
# `jobs.update`
|
||||
# This job runs in response to an issue_comment and will push a new commit
|
||||
# to update the spelling metadata.
|
||||
#
|
||||
# `with.experimental_apply_changes_via_bot`
|
||||
# Tells the action to support and generate messages that enable it
|
||||
# to make a commit to update the spelling metadata.
|
||||
#
|
||||
# `with.ssh_key`
|
||||
# In order to trigger workflows when the commit is made, you can provide a
|
||||
# secret (typically, a write-enabled github deploy key).
|
||||
#
|
||||
# For background, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-with-deploy-key
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags-ignore:
|
||||
- '**'
|
||||
paths:
|
||||
- 'packages/docs/**'
|
||||
- '.github/workflows/docs-spelling.yml'
|
||||
- '.github/actions/docs-spelling/**'
|
||||
pull_request_target:
|
||||
branches:
|
||||
- '**'
|
||||
tags-ignore:
|
||||
- '**'
|
||||
paths:
|
||||
- 'packages/docs/**'
|
||||
- '.github/workflows/docs-spelling.yml'
|
||||
- '.github/actions/docs-spelling/**'
|
||||
types:
|
||||
- 'opened'
|
||||
- 'reopened'
|
||||
- 'synchronize'
|
||||
issue_comment:
|
||||
types:
|
||||
- 'created'
|
||||
|
||||
jobs:
|
||||
spelling:
|
||||
name: Check Spelling
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: read
|
||||
security-events: write
|
||||
outputs:
|
||||
followup: ${{ steps.spelling.outputs.followup }}
|
||||
runs-on: ubuntu-latest
|
||||
if: "contains(github.event_name, 'pull_request') || github.event_name == 'push'"
|
||||
concurrency:
|
||||
group: spelling-${{ github.event.pull_request.number || github.ref }}
|
||||
# note: If you use only_check_changed_files, you do not want cancel-in-progress
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: check-spelling
|
||||
id: spelling
|
||||
uses: check-spelling/check-spelling@main
|
||||
with:
|
||||
suppress_push_for_open_pull_request: 1
|
||||
checkout: true
|
||||
check_file_names: 1
|
||||
spell_check_this: check-spelling/spell-check-this@prerelease
|
||||
post_comment: 0
|
||||
use_magic_file: 1
|
||||
experimental_apply_changes_via_bot: 1
|
||||
use_sarif: 1
|
||||
extra_dictionary_limit: 12
|
||||
check_extra_dictionaries: ''
|
||||
extra_dictionaries: cspell:cpp/src/cpp.txt
|
||||
cspell:software-terms/src/software-terms.txt
|
||||
cspell:python/src/python/python-lib.txt
|
||||
cspell:node/node.txt
|
||||
cspell:filetypes/filetypes.txt
|
||||
cspell:aws/aws.txt
|
||||
cspell:typescript/dict/typescript.txt
|
||||
cspell:npm/dict/npm.txt
|
||||
cspell:fullstack/dict/fullstack.txt
|
||||
cspell:html/dict/html.txt
|
||||
cspell:css/dict/css.txt
|
||||
config: .github/actions/docs-spelling
|
||||
|
||||
comment-push:
|
||||
name: Report (Push)
|
||||
# If your workflow isn't running on push, you can remove this job
|
||||
runs-on: ubuntu-latest
|
||||
needs: spelling
|
||||
permissions:
|
||||
contents: write
|
||||
if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push'
|
||||
steps:
|
||||
- name: comment
|
||||
uses: check-spelling/check-spelling@main
|
||||
with:
|
||||
checkout: true
|
||||
spell_check_this: check-spelling/spell-check-this@prerelease
|
||||
task: ${{ needs.spelling.outputs.followup }}
|
||||
config: .github/actions/docs-spelling
|
||||
|
||||
comment-pr:
|
||||
name: Report (PR)
|
||||
# If you workflow isn't running on pull_request*, you can remove this job
|
||||
runs-on: ubuntu-latest
|
||||
needs: spelling
|
||||
permissions:
|
||||
pull-requests: write
|
||||
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
|
||||
steps:
|
||||
- name: comment
|
||||
uses: check-spelling/check-spelling@main
|
||||
with:
|
||||
checkout: true
|
||||
spell_check_this: check-spelling/spell-check-this@prerelease
|
||||
task: ${{ needs.spelling.outputs.followup }}
|
||||
experimental_apply_changes_via_bot: 1
|
||||
config: .github/actions/docs-spelling
|
||||
|
||||
update:
|
||||
name: Update PR
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '@check-spelling-bot apply')
|
||||
}}
|
||||
concurrency:
|
||||
group: spelling-update-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: apply spelling updates
|
||||
uses: check-spelling/check-spelling@main
|
||||
with:
|
||||
experimental_apply_changes_via_bot: 1
|
||||
checkout: true
|
||||
ssh_key: '${{ secrets.CHECK_SPELLING }}'
|
||||
config: .github/actions/docs-spelling
|
||||
14
.github/workflows/e2e-test.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
outputs:
|
||||
netlify_url: ${{ steps.netlify.outputs.url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Wait for Netlify build to finish
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Trust the repository directory
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
run: yarn e2e
|
||||
env:
|
||||
E2E_START_URL: ${{ needs.netlify.outputs.netlify_url }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
if: always()
|
||||
with:
|
||||
name: desktop-client-test-results
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Trust the repository directory
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
- name: Run Desktop app E2E Tests
|
||||
run: |
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
if: always()
|
||||
with:
|
||||
name: desktop-app-test-results
|
||||
@@ -80,14 +80,14 @@ jobs:
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run VRT Tests on Netlify URL
|
||||
run: yarn vrt
|
||||
env:
|
||||
E2E_START_URL: ${{ needs.netlify.outputs.netlify_url }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
if: always()
|
||||
with:
|
||||
name: desktop-client-test-results
|
||||
|
||||
10
.github/workflows/electron-master.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
run: pip.exe install setuptools
|
||||
- if: ${{ ! startsWith(matrix.os, 'windows') }}
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
if: ${{ ! startsWith(matrix.os, 'macos') }}
|
||||
run: ./bin/package-electron
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}
|
||||
path: |
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
- name: Upload Windows Store Build
|
||||
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}-appx
|
||||
path: |
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
run: |
|
||||
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
||||
- name: Add to new release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
with:
|
||||
draft: true
|
||||
body: |
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
Install-Module -Name StoreBroker -AcceptLicense -Force -Scope CurrentUser -Verbose
|
||||
|
||||
- name: Download Microsoft Store artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: actual-electron-windows-latest-appx
|
||||
|
||||
|
||||
69
.github/workflows/electron-pr.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
run: pip.exe install setuptools
|
||||
- if: ${{ ! startsWith(matrix.os, 'windows') }}
|
||||
@@ -46,19 +46,66 @@ jobs:
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron
|
||||
run: ./bin/package-electron
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
- name: Upload Linux x64 AppImage
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
!packages/desktop-electron/dist/Actual-windows.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
name: Actual-linux-x86_64.AppImage
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-linux-x86_64.AppImage
|
||||
|
||||
- name: Upload Linux arm64 AppImage
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-linux-arm64.AppImage
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-linux-arm64.AppImage
|
||||
|
||||
- name: Upload Linux x64 flatpak
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-linux-x86_64.flatpak
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-linux-x86_64.flatpak
|
||||
|
||||
- name: Upload Windows x32 exe
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-windows-ia32.exe
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-windows-ia32.exe
|
||||
|
||||
- name: Upload Windows x64 exe
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-windows-x64.exe
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-windows-x64.exe
|
||||
|
||||
- name: Upload Windows arm64 exe
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-windows-arm64.exe
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-windows-arm64.exe
|
||||
|
||||
- name: Upload Mac x64 dmg
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-mac-x64.dmg
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-mac-x64.dmg
|
||||
|
||||
- name: Upload Mac arm64 dmg
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-mac-arm64.dmg
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-mac-arm64.dmg
|
||||
|
||||
- name: Upload Windows Store Build
|
||||
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}-appx
|
||||
path: |
|
||||
|
||||
4
.github/workflows/generate-release-pr.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
- name: Bump package versions
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
echo "version=$NEW_WEB_VERSION" >> "$GITHUB_OUTPUT"
|
||||
- name: Create PR
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
commit-message: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
|
||||
title: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
if: github.repository == 'actualbudget/actual'
|
||||
steps:
|
||||
- name: Check out main repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
path: actual
|
||||
- name: Set up environment
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
push \
|
||||
actualbudget/actual
|
||||
- name: Check out updated translations
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ssh-key: ${{ secrets.STRING_IMPORT_DEPLOY_KEY }}
|
||||
repository: actualbudget/translations
|
||||
|
||||
@@ -9,16 +9,17 @@ jobs:
|
||||
if: ${{ github.event.label.name == 'feature' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
- uses: actions-ecosystem/action-add-labels@bd52874380e3909a1ac983768df6976535ece7f8 # v1.1.0
|
||||
with:
|
||||
labels: needs votes
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Add reactions
|
||||
uses: aidan-mundy/react-to-issue@v1.1.1
|
||||
uses: aidan-mundy/react-to-issue@109392cac5159c2df6c47c8ab3b5d6b708852fe5 # v1.1.2
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
reactions: '+1'
|
||||
- name: Create comment
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
|
||||
@@ -24,8 +24,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# This is not a security concern because we have approved & merged the PR
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Handle feature requests
|
||||
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
if: ${{ !contains(github.event.issue.labels.*.name, 'feature') && contains(github.event.issue.labels.*.name, 'help wanted') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
- uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0
|
||||
with:
|
||||
labels: help wanted
|
||||
|
||||
2
.github/workflows/netlify-release.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repository Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
131
.github/workflows/publish-nightly-electron.yml
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
name: Publish nightly desktop app
|
||||
|
||||
# Publish nightly version of desktop app - Runs every day at midnight
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-22.04
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
run: pip.exe install setuptools
|
||||
|
||||
- if: ${{ ! startsWith(matrix.os, 'windows') }}
|
||||
run: |
|
||||
mkdir .venv
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python3 -m pip install setuptools
|
||||
|
||||
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install flatpak -y
|
||||
sudo apt-get install flatpak-builder -y
|
||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
sudo flatpak install org.freedesktop.Sdk//24.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform//24.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp//24.08 -y
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Update package versions
|
||||
run: |
|
||||
# Get new nightly version
|
||||
NEW_DESKTOP_APP_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-electron/package.json --type nightly)
|
||||
|
||||
# Set package version
|
||||
npm version $NEW_DESKTOP_APP_VERSION --no-git-tag-version --workspace=desktop-electron --no-workspaces-update
|
||||
|
||||
- name: Build Electron for Mac
|
||||
if: ${{ startsWith(matrix.os, 'macos') }}
|
||||
run: ./bin/package-electron
|
||||
env:
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
- name: Build Electron
|
||||
if: ${{ ! startsWith(matrix.os, 'macos') }}
|
||||
run: ./bin/package-electron
|
||||
|
||||
- name: Upload Linux x64 AppImage
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-linux-x86_64.AppImage
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-linux-x86_64.AppImage
|
||||
|
||||
- name: Upload Linux arm64 AppImage
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-linux-arm64.AppImage
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-linux-arm64.AppImage
|
||||
|
||||
- name: Upload Windows x32 exe
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-windows-ia32.exe
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-windows-ia32.exe
|
||||
|
||||
- name: Upload Windows x64 exe
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-windows-x64.exe
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-windows-x64.exe
|
||||
|
||||
- name: Upload Windows arm64 exe
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-windows-arm64.exe
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-windows-arm64.exe
|
||||
|
||||
- name: Upload Mac x64 dmg
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-mac-x64.dmg
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-mac-x64.dmg
|
||||
|
||||
- name: Upload Mac arm64 dmg
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Actual-mac-arm64.dmg
|
||||
if-no-files-found: ignore
|
||||
path: packages/desktop-electron/dist/Actual-mac-arm64.dmg
|
||||
|
||||
- name: Upload Windows Store Build
|
||||
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}-appx
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.appx
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
name: Build and pack npm packages
|
||||
if: github.event.repository.fork == false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
|
||||
|
||||
- name: Upload package artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: npm-packages
|
||||
path: |
|
||||
@@ -66,12 +66,12 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Download the artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: npm-packages
|
||||
|
||||
- name: Setup node and npm registry
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
8
.github/workflows/publish-npm-packages.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and pack npm packages
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
|
||||
|
||||
- name: Upload package artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: npm-packages
|
||||
path: |
|
||||
@@ -49,12 +49,12 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Download the artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: npm-packages
|
||||
|
||||
- name: Setup node and npm registry
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
21
.github/workflows/release-notes.yml
vendored
@@ -12,10 +12,21 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Check release notes
|
||||
if: startsWith(github.head_ref, 'release/') == false
|
||||
uses: actualbudget/actions/release-notes/check@main
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
run: |
|
||||
git fetch origin ${{ github.base_ref }}
|
||||
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
||||
NON_DOCS_FILES=$(echo "$CHANGED_FILES" | grep -v -e "^packages/docs/" -e "^\.github/actions/docs-spelling/" || true)
|
||||
|
||||
if [ -z "$NON_DOCS_FILES" ] && [ -n "$CHANGED_FILES" ]; then
|
||||
echo "only_docs=true" >> $GITHUB_OUTPUT
|
||||
echo "only documentation files changed, skipping release notes check"
|
||||
else
|
||||
echo "only_docs=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Generate release notes
|
||||
if: startsWith(github.head_ref, 'release/') == true
|
||||
uses: actualbudget/actions/release-notes/generate@main
|
||||
|
||||
118
.github/workflows/size-compare.yml
vendored
@@ -26,40 +26,73 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Wait for ${{github.base_ref}} build to succeed
|
||||
uses: fountainhead/action-wait-for-check@v1.2.0
|
||||
id: master-build
|
||||
- name: Checkout base branch
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
|
||||
- name: Wait for ${{github.base_ref}} web build to succeed
|
||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
||||
id: master-web-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: web
|
||||
ref: ${{github.base_ref}}
|
||||
- name: Wait for ${{github.base_ref}} API build to succeed
|
||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
||||
id: master-api-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: api
|
||||
ref: ${{github.base_ref}}
|
||||
|
||||
- name: Wait for PR build to succeed
|
||||
uses: fountainhead/action-wait-for-check@v1.2.0
|
||||
id: wait-for-build
|
||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
||||
id: wait-for-web-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: web
|
||||
ref: ${{github.event.pull_request.head.sha}}
|
||||
- name: Wait for API PR build to succeed
|
||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
||||
id: wait-for-api-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: api
|
||||
ref: ${{github.event.pull_request.head.sha}}
|
||||
|
||||
- name: Report build failure
|
||||
if: steps.wait-for-build.outputs.conclusion == 'failure'
|
||||
if: steps.wait-for-web-build.outputs.conclusion == 'failure' || steps.wait-for-api-build.outputs.conclusion == 'failure'
|
||||
run: |
|
||||
echo "Build failed on PR branch or ${{github.base_ref}}"
|
||||
exit 1
|
||||
- name: Download build artifact from ${{github.base_ref}}
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
id: pr-build
|
||||
|
||||
- name: Download web build artifact from ${{github.base_ref}}
|
||||
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
||||
id: pr-web-build
|
||||
with:
|
||||
branch: ${{github.base_ref}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: build-stats
|
||||
path: base
|
||||
|
||||
- name: Download build artifact from PR
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
- name: Download API build artifact from ${{github.base_ref}}
|
||||
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
||||
id: pr-api-build
|
||||
with:
|
||||
branch: ${{github.base_ref}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: api-build-stats
|
||||
path: base
|
||||
- name: Download build stats from PR
|
||||
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
||||
with:
|
||||
pr: ${{github.event.pull_request.number}}
|
||||
workflow: build.yml
|
||||
@@ -67,25 +100,46 @@ jobs:
|
||||
name: build-stats
|
||||
path: head
|
||||
allow_forks: true
|
||||
|
||||
- name: Download API stats from PR
|
||||
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
|
||||
with:
|
||||
pr: ${{github.event.pull_request.number}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: api-build-stats
|
||||
path: head
|
||||
allow_forks: true
|
||||
- name: Strip content hashes from stats files
|
||||
run: |
|
||||
sed -i -E 's/index\.[0-9a-zA-Z_-]{8,}\./index./g' ./head/web-stats.json
|
||||
sed -i -E 's/\.[0-9a-zA-Z_-]{8,}\.chunk\././g' ./head/web-stats.json
|
||||
sed -i -E 's/\.[0-9a-f]{8,}\././g' ./head/*.json
|
||||
sed -i -E 's/index\.[0-9a-zA-Z_-]{8,}\./index./g' ./base/web-stats.json
|
||||
sed -i -E 's/\.[0-9a-zA-Z_-]{8,}\.chunk\././g' ./base/web-stats.json
|
||||
sed -i -E 's/\.[0-9a-f]{8,}\././g' ./base/*.json
|
||||
- uses: twk3/rollup-size-compare-action@v1.1.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
current-stats-json-path: ./head/web-stats.json
|
||||
base-stats-json-path: ./base/web-stats.json
|
||||
title: desktop-client
|
||||
|
||||
- uses: twk3/rollup-size-compare-action@v1.1.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
current-stats-json-path: ./head/loot-core-stats.json
|
||||
base-stats-json-path: ./base/loot-core-stats.json
|
||||
title: loot-core
|
||||
if [ -f ./head/web-stats.json ]; then
|
||||
sed -i -E 's/index\.[0-9a-zA-Z_-]{8,}\./index./g' ./head/web-stats.json
|
||||
sed -i -E 's/\.[0-9a-zA-Z_-]{8,}\.chunk\././g' ./head/web-stats.json
|
||||
fi
|
||||
if [ -f ./base/web-stats.json ]; then
|
||||
sed -i -E 's/index\.[0-9a-zA-Z_-]{8,}\./index./g' ./base/web-stats.json
|
||||
sed -i -E 's/\.[0-9a-zA-Z_-]{8,}\.chunk\././g' ./base/web-stats.json
|
||||
fi
|
||||
for file in ./head/*.json ./base/*.json; do
|
||||
if [ -f "$file" ]; then
|
||||
sed -i -E 's/\.[0-9a-f]{8,}\././g' "$file"
|
||||
fi
|
||||
done
|
||||
- name: Generate combined bundle stats comment
|
||||
run: |
|
||||
node packages/ci-actions/bin/bundle-stats-comment.mjs \
|
||||
--base desktop-client=./base/web-stats.json \
|
||||
--base loot-core=./base/loot-core-stats.json \
|
||||
--base api=./base/api-stats.json \
|
||||
--head desktop-client=./head/web-stats.json \
|
||||
--head loot-core=./head/loot-core-stats.json \
|
||||
--head api=./head/api-stats.json \
|
||||
--identifier combined > bundle-stats-comment.md
|
||||
- name: Post combined bundle stats comment
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
node packages/ci-actions/bin/update-bundle-stats-comment.mjs \
|
||||
--comment-file bundle-stats-comment.md \
|
||||
--identifier '<!--- bundlestats-action-comment key:combined --->'
|
||||
|
||||
6
.github/workflows/stale.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
with:
|
||||
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
stale-wip:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
with:
|
||||
stale-pr-message: ':wave: Hi! It looks like this PR has not had any changes for a week now. Would you like someone to review this PR? If so - please remove the "[WIP]" prefix from the PR title. That will let the community know that this PR is open for a review.'
|
||||
days-before-stale: 7
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
stale-needs-info:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
with:
|
||||
stale-issue-label: 'needs info'
|
||||
days-before-stale: -1
|
||||
|
||||
25
.github/workflows/vrt-update-apply.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Download patch artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
path: /tmp/artifacts
|
||||
|
||||
- name: Download metadata artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
@@ -37,17 +37,14 @@ jobs:
|
||||
- name: Extract metadata
|
||||
id: metadata
|
||||
run: |
|
||||
# Find the metadata directory (will be vrt-metadata-{PR_NUMBER})
|
||||
METADATA_DIR=$(find /tmp/metadata -mindepth 1 -maxdepth 1 -type d | head -n 1)
|
||||
|
||||
if [ -z "$METADATA_DIR" ]; then
|
||||
if [ ! -f "/tmp/metadata/pr-number.txt" ]; then
|
||||
echo "No metadata found, skipping..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PR_NUMBER=$(cat "$METADATA_DIR/pr-number.txt")
|
||||
HEAD_REF=$(cat "$METADATA_DIR/head-ref.txt")
|
||||
HEAD_REPO=$(cat "$METADATA_DIR/head-repo.txt")
|
||||
PR_NUMBER=$(cat "/tmp/metadata/pr-number.txt")
|
||||
HEAD_REF=$(cat "/tmp/metadata/head-ref.txt")
|
||||
HEAD_REPO=$(cat "/tmp/metadata/head-repo.txt")
|
||||
|
||||
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT"
|
||||
@@ -57,7 +54,7 @@ jobs:
|
||||
|
||||
- name: Checkout fork branch
|
||||
if: steps.metadata.outputs.pr_number != ''
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
repository: ${{ steps.metadata.outputs.head_repo }}
|
||||
ref: ${{ steps.metadata.outputs.head_ref }}
|
||||
@@ -68,9 +65,7 @@ jobs:
|
||||
if: steps.metadata.outputs.pr_number != ''
|
||||
id: apply
|
||||
run: |
|
||||
# Find the patch file
|
||||
PATCH_DIR=$(find /tmp/artifacts -mindepth 1 -maxdepth 1 -type d | head -n 1)
|
||||
PATCH_FILE="$PATCH_DIR/vrt-update.patch"
|
||||
PATCH_FILE="/tmp/artifacts/vrt-update.patch"
|
||||
|
||||
if [ ! -f "$PATCH_FILE" ]; then
|
||||
echo "No patch file found"
|
||||
@@ -132,7 +127,7 @@ jobs:
|
||||
|
||||
- name: Comment on PR - Success
|
||||
if: steps.apply.outputs.applied == 'true'
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
@@ -144,7 +139,7 @@ jobs:
|
||||
|
||||
- name: Comment on PR - Failure
|
||||
if: failure() && steps.metadata.outputs.pr_number != ''
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const error = `${{ steps.apply.outputs.error }}` || 'Unknown error occurred';
|
||||
|
||||
6
.github/workflows/vrt-update-generate.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
|
||||
- name: Upload patch artifact
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: vrt-patch-${{ github.event.pull_request.number }}
|
||||
path: vrt-update.patch
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
|
||||
- name: Upload PR metadata
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: vrt-metadata-${{ github.event.pull_request.number }}
|
||||
path: pr-metadata/
|
||||
|
||||
@@ -10,6 +10,7 @@ packages/desktop-client/bundle.browser.js
|
||||
packages/desktop-client/stats.json
|
||||
packages/desktop-client/.swc/
|
||||
packages/desktop-client/build/
|
||||
packages/desktop-client/dev-dist/
|
||||
packages/desktop-client/locale/
|
||||
packages/desktop-client/build-electron/
|
||||
packages/desktop-client/build-stats/
|
||||
@@ -26,5 +27,10 @@ packages/loot-core/**/node_modules/*
|
||||
packages/loot-core/**/lib-dist/*
|
||||
packages/loot-core/**/proto/*
|
||||
packages/sync-server/coverage/
|
||||
packages/sync-server/user-files/
|
||||
packages/sync-server/server-files/
|
||||
.yarn/*
|
||||
upcoming-release-notes/*
|
||||
|
||||
# temporary
|
||||
packages/docs/*
|
||||
|
||||
@@ -7,7 +7,7 @@ This guide provides comprehensive information for AI agents (like Cursor) workin
|
||||
**Actual Budget** is a local-first personal finance tool written in TypeScript/JavaScript. It's 100% free and open-source with synchronization capabilities across devices.
|
||||
|
||||
- **Repository**: https://github.com/actualbudget/actual
|
||||
- **Community Docs**: https://github.com/actualbudget/docs or https://actualbudget.org/docs
|
||||
- **Community Docs**: https://github.com/actualbudget/actual/tree/master/packages/docs or https://actualbudget.org/docs
|
||||
- **License**: MIT
|
||||
- **Primary Language**: TypeScript (with React)
|
||||
- **Build System**: Yarn 4 workspaces (monorepo)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import globals from 'globals';
|
||||
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import pluginImport from 'eslint-plugin-import';
|
||||
import pluginJSXA11y from 'eslint-plugin-jsx-a11y';
|
||||
import pluginReact from 'eslint-plugin-react';
|
||||
import pluginReactHooks from 'eslint-plugin-react-hooks';
|
||||
import pluginTypescript from 'typescript-eslint';
|
||||
import pluginTypescriptPaths from 'eslint-plugin-typescript-paths';
|
||||
import pluginActual from './packages/eslint-plugin-actual/lib/index.js';
|
||||
import globals from 'globals';
|
||||
import pluginTypescript from 'typescript-eslint';
|
||||
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import pluginActual from './packages/eslint-plugin-actual/lib/index.js';
|
||||
|
||||
const confusingBrowserGlobals = [
|
||||
// https://github.com/facebook/create-react-app/tree/main/packages/confusing-browser-globals
|
||||
@@ -75,34 +75,33 @@ const confusingBrowserGlobals = [
|
||||
export default defineConfig(
|
||||
{
|
||||
ignores: [
|
||||
//temporary
|
||||
'packages/docs',
|
||||
|
||||
'packages/api/app/bundle.api.js',
|
||||
'packages/api/app/stats.json',
|
||||
'packages/api/dist',
|
||||
'packages/api/@types',
|
||||
'packages/api/migrations',
|
||||
'packages/crdt/dist',
|
||||
'packages/component-library/src/icons/**/*',
|
||||
'packages/desktop-client/bundle.browser.js',
|
||||
'packages/desktop-client/build/',
|
||||
'packages/desktop-client/dev-dist/',
|
||||
'packages/desktop-client/service-worker/*',
|
||||
'packages/desktop-client/build-electron/',
|
||||
'packages/desktop-client/build-stats/',
|
||||
'packages/desktop-client/public/kcab/',
|
||||
'packages/desktop-client/public/data/',
|
||||
'packages/desktop-client/**/node_modules/*',
|
||||
'packages/desktop-client/node_modules/',
|
||||
'packages/desktop-client/test-results/',
|
||||
'packages/desktop-client/playwright-report/',
|
||||
'packages/desktop-electron/client-build/',
|
||||
'packages/desktop-electron/build/',
|
||||
'packages/desktop-electron/dist/',
|
||||
'packages/loot-core/**/node_modules/*',
|
||||
'packages/loot-core/**/lib-dist/*',
|
||||
'packages/loot-core/**/proto/*',
|
||||
'packages/sync-server/build/',
|
||||
'packages/plugins-service/dist/',
|
||||
'packages/sync-server/user-files/',
|
||||
'packages/sync-server/server-files/',
|
||||
'.yarn/*',
|
||||
'.github/*',
|
||||
'**/build/',
|
||||
'**/dist/',
|
||||
'**/node_modules/',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -164,7 +163,7 @@ export default defineConfig(
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,ts,jsx,tsx}'],
|
||||
files: ['**/*.{js,ts,jsx,tsx,mjs,mts}'],
|
||||
plugins: {
|
||||
'jsx-a11y': pluginJSXA11y,
|
||||
'react-hooks': pluginReactHooks,
|
||||
@@ -448,7 +447,7 @@ export default defineConfig(
|
||||
'react-hooks/exhaustive-deps': [
|
||||
'warn',
|
||||
{
|
||||
additionalHooks: '(useQuery)',
|
||||
additionalHooks: '(useQuery|useEffectAfterMount)',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -665,7 +664,7 @@ export default defineConfig(
|
||||
'warn',
|
||||
{
|
||||
types: {
|
||||
// forbid FC as superflous
|
||||
// forbid FC as superfluous
|
||||
FunctionComponent: {
|
||||
message:
|
||||
'Type the props argument and let TS infer or use ComponentType for a component prop',
|
||||
@@ -710,14 +709,18 @@ export default defineConfig(
|
||||
|
||||
// Allow configuring vitest with default exports (recommended as per vitest docs)
|
||||
{
|
||||
files: ['**/vitest.config.ts', '**/vitest.web.config.ts'],
|
||||
files: [
|
||||
'**/vitest.config.{ts,mts}',
|
||||
'**/vitest.web.config.ts',
|
||||
'**/vite.config.{ts,mts}',
|
||||
'eslint.config.mjs',
|
||||
],
|
||||
rules: {
|
||||
'import/no-anonymous-default-export': 'off',
|
||||
'import/no-default-export': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
{},
|
||||
{
|
||||
// TODO: fix the issues in these files
|
||||
files: [
|
||||
|
||||
24
package.json
@@ -23,6 +23,7 @@
|
||||
"start:server-monitor": "yarn workspace @actual-app/sync-server start-monitor",
|
||||
"start:server-dev": "NODE_ENV=development BROWSER_OPEN=localhost:5006 yarn npm-run-all --parallel 'start:server-monitor' 'start'",
|
||||
"start:desktop": "yarn desktop-dependencies && npm-run-all --parallel 'start:desktop-*'",
|
||||
"start:docs": "yarn workspace docs start",
|
||||
"desktop-dependencies": "npm-run-all --parallel rebuild-electron build:browser-backend build:plugins-service",
|
||||
"start:desktop-node": "yarn workspace loot-core watch:node",
|
||||
"start:desktop-client": "yarn workspace @actual-app/web watch",
|
||||
@@ -38,6 +39,7 @@
|
||||
"build:desktop": "./bin/package-electron",
|
||||
"build:plugins-service": "yarn workspace plugins-service build",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
"build:docs": "yarn workspace docs build",
|
||||
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
|
||||
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
|
||||
"test": "lage test --continue",
|
||||
@@ -57,34 +59,34 @@
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@types/node": "^22.18.11",
|
||||
"@octokit/rest": "^22.0.1",
|
||||
"@types/node": "^22.19.1",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"@typescript-eslint/parser": "^8.46.4",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.0",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-typescript-paths": "^0.0.33",
|
||||
"globals": "^16.4.0",
|
||||
"globals": "^16.5.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"husky": "^9.1.7",
|
||||
"lage": "^2.14.14",
|
||||
"lint-staged": "^16.2.3",
|
||||
"minimatch": "^10.0.3",
|
||||
"lage": "^2.14.15",
|
||||
"lint-staged": "^16.2.6",
|
||||
"minimatch": "^10.1.1",
|
||||
"node-jq": "^6.3.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"p-limit": "^7.1.1",
|
||||
"p-limit": "^7.2.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prompts": "^2.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.0",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"typescript-strict-plugin": "^2.4.4"
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// @ts-strict-ignore
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
import { type RuleEntity } from 'loot-core/types/models';
|
||||
|
||||
import * as api from './index';
|
||||
|
||||
const budgetName = 'test-budget';
|
||||
@@ -282,7 +283,7 @@ describe('API CRUD operations', () => {
|
||||
expect(await api.getAccountBalance(accountId2)).toEqual(0);
|
||||
|
||||
await api.updateAccount(accountId1, { offbudget: false });
|
||||
await api.closeAccount(accountId1, accountId2, null);
|
||||
await api.closeAccount(accountId1, accountId2);
|
||||
await api.deleteAccount(accountId2);
|
||||
|
||||
// accounts successfully updated, and one of them deleted
|
||||
@@ -505,7 +506,7 @@ describe('API CRUD operations', () => {
|
||||
...rule,
|
||||
stage: 'post',
|
||||
conditionsOp: 'or',
|
||||
};
|
||||
} satisfies RuleEntity;
|
||||
expect(await api.updateRule(updatedRule)).toEqual(updatedRule);
|
||||
|
||||
expect(await api.getRules()).toEqual(
|
||||
@@ -719,7 +720,7 @@ describe('API CRUD operations', () => {
|
||||
|
||||
// Test without notes
|
||||
const transactionsWithoutNotes = [
|
||||
{ date: '2023-11-03', imported_id: '11', amount: 100, notes: null },
|
||||
{ date: '2023-11-03', imported_id: '11', amount: 100 },
|
||||
];
|
||||
|
||||
const addResultWithoutNotes = await api.addTransactions(
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
// @ts-strict-ignore
|
||||
import type {
|
||||
APIAccountEntity,
|
||||
APICategoryEntity,
|
||||
APICategoryGroupEntity,
|
||||
APIFileEntity,
|
||||
APIPayeeEntity,
|
||||
APIScheduleEntity,
|
||||
} from 'loot-core/server/api-models';
|
||||
import type { Query } from 'loot-core/shared/query';
|
||||
import type { Handlers } from 'loot-core/types/handlers';
|
||||
import type { ImportTransactionEntity } from 'loot-core/types/models/import-transaction';
|
||||
import type {
|
||||
ImportTransactionEntity,
|
||||
RuleEntity,
|
||||
TransactionEntity,
|
||||
} from 'loot-core/types/models';
|
||||
|
||||
import * as injected from './injected';
|
||||
|
||||
@@ -13,8 +25,11 @@ function send<K extends keyof Handlers, T extends Handlers[K]>(
|
||||
return injected.send(name, args);
|
||||
}
|
||||
|
||||
export async function runImport(name, func) {
|
||||
await send('api/start-import', { budgetName: name });
|
||||
export async function runImport(
|
||||
budgetName: APIFileEntity['name'],
|
||||
func: () => Promise<void>,
|
||||
) {
|
||||
await send('api/start-import', { budgetName });
|
||||
try {
|
||||
await func();
|
||||
} catch (e) {
|
||||
@@ -24,11 +39,14 @@ export async function runImport(name, func) {
|
||||
await send('api/finish-import');
|
||||
}
|
||||
|
||||
export async function loadBudget(budgetId) {
|
||||
export async function loadBudget(budgetId: string) {
|
||||
return send('api/load-budget', { id: budgetId });
|
||||
}
|
||||
|
||||
export async function downloadBudget(syncId, { password }: { password? } = {}) {
|
||||
export async function downloadBudget(
|
||||
syncId: string,
|
||||
{ password }: { password?: string } = {},
|
||||
) {
|
||||
return send('api/download-budget', { syncId, password });
|
||||
}
|
||||
|
||||
@@ -40,11 +58,13 @@ export async function sync() {
|
||||
return send('api/sync');
|
||||
}
|
||||
|
||||
export async function runBankSync(args?: { accountId: string }) {
|
||||
export async function runBankSync(args?: {
|
||||
accountId: APIAccountEntity['id'];
|
||||
}) {
|
||||
return send('api/bank-sync', args);
|
||||
}
|
||||
|
||||
export async function batchBudgetUpdates(func) {
|
||||
export async function batchBudgetUpdates(func: () => Promise<void>) {
|
||||
await send('api/batch-budget-start');
|
||||
try {
|
||||
await func();
|
||||
@@ -57,11 +77,11 @@ export async function batchBudgetUpdates(func) {
|
||||
* @deprecated Please use `aqlQuery` instead.
|
||||
* This function will be removed in a future release.
|
||||
*/
|
||||
export function runQuery(query) {
|
||||
export function runQuery(query: Query) {
|
||||
return send('api/query', { query: query.serialize() });
|
||||
}
|
||||
|
||||
export function aqlQuery(query) {
|
||||
export function aqlQuery(query: Query) {
|
||||
return send('api/query', { query: query.serialize() });
|
||||
}
|
||||
|
||||
@@ -69,22 +89,33 @@ export function getBudgetMonths() {
|
||||
return send('api/budget-months');
|
||||
}
|
||||
|
||||
export function getBudgetMonth(month) {
|
||||
export function getBudgetMonth(month: string) {
|
||||
return send('api/budget-month', { month });
|
||||
}
|
||||
|
||||
export function setBudgetAmount(month, categoryId, value) {
|
||||
export function setBudgetAmount(
|
||||
month: string,
|
||||
categoryId: APICategoryEntity['id'],
|
||||
value: number,
|
||||
) {
|
||||
return send('api/budget-set-amount', { month, categoryId, amount: value });
|
||||
}
|
||||
|
||||
export function setBudgetCarryover(month, categoryId, flag) {
|
||||
export function setBudgetCarryover(
|
||||
month: string,
|
||||
categoryId: APICategoryEntity['id'],
|
||||
flag: boolean,
|
||||
) {
|
||||
return send('api/budget-set-carryover', { month, categoryId, flag });
|
||||
}
|
||||
|
||||
export function addTransactions(
|
||||
accountId,
|
||||
transactions,
|
||||
{ learnCategories = false, runTransfers = false } = {},
|
||||
accountId: APIAccountEntity['id'],
|
||||
transactions: Omit<ImportTransactionEntity, 'account'>[],
|
||||
{
|
||||
learnCategories = false,
|
||||
runTransfers = false,
|
||||
}: { learnCategories?: boolean; runTransfers?: boolean } = {},
|
||||
) {
|
||||
return send('api/transactions-add', {
|
||||
accountId,
|
||||
@@ -100,7 +131,7 @@ export interface ImportTransactionsOpts {
|
||||
}
|
||||
|
||||
export function importTransactions(
|
||||
accountId: string,
|
||||
accountId: APIAccountEntity['id'],
|
||||
transactions: ImportTransactionEntity[],
|
||||
opts: ImportTransactionsOpts = {
|
||||
defaultCleared: true,
|
||||
@@ -115,15 +146,22 @@ export function importTransactions(
|
||||
});
|
||||
}
|
||||
|
||||
export function getTransactions(accountId, startDate, endDate) {
|
||||
export function getTransactions(
|
||||
accountId: APIAccountEntity['id'],
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
) {
|
||||
return send('api/transactions-get', { accountId, startDate, endDate });
|
||||
}
|
||||
|
||||
export function updateTransaction(id, fields) {
|
||||
export function updateTransaction(
|
||||
id: TransactionEntity['id'],
|
||||
fields: Partial<TransactionEntity>,
|
||||
) {
|
||||
return send('api/transaction-update', { id, fields });
|
||||
}
|
||||
|
||||
export function deleteTransaction(id) {
|
||||
export function deleteTransaction(id: TransactionEntity['id']) {
|
||||
return send('api/transaction-delete', { id });
|
||||
}
|
||||
|
||||
@@ -131,15 +169,25 @@ export function getAccounts() {
|
||||
return send('api/accounts-get');
|
||||
}
|
||||
|
||||
export function createAccount(account, initialBalance?) {
|
||||
export function createAccount(
|
||||
account: Omit<APIAccountEntity, 'id'>,
|
||||
initialBalance?: number,
|
||||
) {
|
||||
return send('api/account-create', { account, initialBalance });
|
||||
}
|
||||
|
||||
export function updateAccount(id, fields) {
|
||||
export function updateAccount(
|
||||
id: APIAccountEntity['id'],
|
||||
fields: Partial<APIAccountEntity>,
|
||||
) {
|
||||
return send('api/account-update', { id, fields });
|
||||
}
|
||||
|
||||
export function closeAccount(id, transferAccountId?, transferCategoryId?) {
|
||||
export function closeAccount(
|
||||
id: APIAccountEntity['id'],
|
||||
transferAccountId?: APIAccountEntity['id'],
|
||||
transferCategoryId?: APICategoryEntity['id'],
|
||||
) {
|
||||
return send('api/account-close', {
|
||||
id,
|
||||
transferAccountId,
|
||||
@@ -147,15 +195,15 @@ export function closeAccount(id, transferAccountId?, transferCategoryId?) {
|
||||
});
|
||||
}
|
||||
|
||||
export function reopenAccount(id) {
|
||||
export function reopenAccount(id: APIAccountEntity['id']) {
|
||||
return send('api/account-reopen', { id });
|
||||
}
|
||||
|
||||
export function deleteAccount(id) {
|
||||
export function deleteAccount(id: APIAccountEntity['id']) {
|
||||
return send('api/account-delete', { id });
|
||||
}
|
||||
|
||||
export function getAccountBalance(id, cutoff?) {
|
||||
export function getAccountBalance(id: APIAccountEntity['id'], cutoff?: Date) {
|
||||
return send('api/account-balance', { id, cutoff });
|
||||
}
|
||||
|
||||
@@ -163,15 +211,21 @@ export function getCategoryGroups() {
|
||||
return send('api/category-groups-get');
|
||||
}
|
||||
|
||||
export function createCategoryGroup(group) {
|
||||
export function createCategoryGroup(group: Omit<APICategoryGroupEntity, 'id'>) {
|
||||
return send('api/category-group-create', { group });
|
||||
}
|
||||
|
||||
export function updateCategoryGroup(id, fields) {
|
||||
export function updateCategoryGroup(
|
||||
id: APICategoryGroupEntity['id'],
|
||||
fields: Partial<APICategoryGroupEntity>,
|
||||
) {
|
||||
return send('api/category-group-update', { id, fields });
|
||||
}
|
||||
|
||||
export function deleteCategoryGroup(id, transferCategoryId?) {
|
||||
export function deleteCategoryGroup(
|
||||
id: APICategoryGroupEntity['id'],
|
||||
transferCategoryId?: APICategoryEntity['id'],
|
||||
) {
|
||||
return send('api/category-group-delete', { id, transferCategoryId });
|
||||
}
|
||||
|
||||
@@ -179,15 +233,21 @@ export function getCategories() {
|
||||
return send('api/categories-get', { grouped: false });
|
||||
}
|
||||
|
||||
export function createCategory(category) {
|
||||
export function createCategory(category: Omit<APICategoryEntity, 'id'>) {
|
||||
return send('api/category-create', { category });
|
||||
}
|
||||
|
||||
export function updateCategory(id, fields) {
|
||||
export function updateCategory(
|
||||
id: APICategoryEntity['id'],
|
||||
fields: Partial<APICategoryEntity>,
|
||||
) {
|
||||
return send('api/category-update', { id, fields });
|
||||
}
|
||||
|
||||
export function deleteCategory(id, transferCategoryId?) {
|
||||
export function deleteCategory(
|
||||
id: APICategoryEntity['id'],
|
||||
transferCategoryId?: APICategoryEntity['id'],
|
||||
) {
|
||||
return send('api/category-delete', { id, transferCategoryId });
|
||||
}
|
||||
|
||||
@@ -199,19 +259,25 @@ export function getPayees() {
|
||||
return send('api/payees-get');
|
||||
}
|
||||
|
||||
export function createPayee(payee) {
|
||||
export function createPayee(payee: Omit<APIPayeeEntity, 'id'>) {
|
||||
return send('api/payee-create', { payee });
|
||||
}
|
||||
|
||||
export function updatePayee(id, fields) {
|
||||
export function updatePayee(
|
||||
id: APIPayeeEntity['id'],
|
||||
fields: Partial<APIPayeeEntity>,
|
||||
) {
|
||||
return send('api/payee-update', { id, fields });
|
||||
}
|
||||
|
||||
export function deletePayee(id) {
|
||||
export function deletePayee(id: APIPayeeEntity['id']) {
|
||||
return send('api/payee-delete', { id });
|
||||
}
|
||||
|
||||
export function mergePayees(targetId, mergeIds) {
|
||||
export function mergePayees(
|
||||
targetId: APIPayeeEntity['id'],
|
||||
mergeIds: APIPayeeEntity['id'][],
|
||||
) {
|
||||
return send('api/payees-merge', { targetId, mergeIds });
|
||||
}
|
||||
|
||||
@@ -219,35 +285,39 @@ export function getRules() {
|
||||
return send('api/rules-get');
|
||||
}
|
||||
|
||||
export function getPayeeRules(id) {
|
||||
export function getPayeeRules(id: RuleEntity['id']) {
|
||||
return send('api/payee-rules-get', { id });
|
||||
}
|
||||
|
||||
export function createRule(rule) {
|
||||
export function createRule(rule: Omit<RuleEntity, 'id'>) {
|
||||
return send('api/rule-create', { rule });
|
||||
}
|
||||
|
||||
export function updateRule(rule) {
|
||||
export function updateRule(rule: RuleEntity) {
|
||||
return send('api/rule-update', { rule });
|
||||
}
|
||||
|
||||
export function deleteRule(id: string) {
|
||||
export function deleteRule(id: RuleEntity['id']) {
|
||||
return send('api/rule-delete', id);
|
||||
}
|
||||
|
||||
export function holdBudgetForNextMonth(month, amount) {
|
||||
export function holdBudgetForNextMonth(month: string, amount: number) {
|
||||
return send('api/budget-hold-for-next-month', { month, amount });
|
||||
}
|
||||
|
||||
export function resetBudgetHold(month) {
|
||||
export function resetBudgetHold(month: string) {
|
||||
return send('api/budget-reset-hold', { month });
|
||||
}
|
||||
|
||||
export function createSchedule(schedule) {
|
||||
export function createSchedule(schedule: Omit<APIScheduleEntity, 'id'>) {
|
||||
return send('api/schedule-create', schedule);
|
||||
}
|
||||
|
||||
export function updateSchedule(id, fields, resetNextDate?: boolean) {
|
||||
export function updateSchedule(
|
||||
id: APIScheduleEntity['id'],
|
||||
fields: Partial<APIScheduleEntity>,
|
||||
resetNextDate?: boolean,
|
||||
) {
|
||||
return send('api/schedule-update', {
|
||||
id,
|
||||
fields,
|
||||
@@ -255,7 +325,7 @@ export function updateSchedule(id, fields, resetNextDate?: boolean) {
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteSchedule(scheduleId) {
|
||||
export function deleteSchedule(scheduleId: APIScheduleEntity['id']) {
|
||||
return send('api/schedule-delete', scheduleId);
|
||||
}
|
||||
|
||||
@@ -263,7 +333,10 @@ export function getSchedules() {
|
||||
return send('api/schedules-get');
|
||||
}
|
||||
|
||||
export function getIDByName(type, name) {
|
||||
export function getIDByName(
|
||||
type: 'accounts' | 'schedules' | 'categories' | 'payees',
|
||||
name: string,
|
||||
) {
|
||||
return send('api/get-id-by-name', { type, name });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "25.10.0",
|
||||
"version": "25.11.0",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
@@ -32,6 +32,6 @@
|
||||
"devDependencies": {
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,6 @@ export default {
|
||||
// print only console.error
|
||||
return type === 'stderr';
|
||||
},
|
||||
poolOptions: {
|
||||
threads: {
|
||||
maxThreads: 2,
|
||||
minThreads: 1,
|
||||
},
|
||||
},
|
||||
maxWorkers: 2,
|
||||
},
|
||||
};
|
||||
|
||||
678
packages/ci-actions/bin/bundle-stats-comment.mjs
Normal file
@@ -0,0 +1,678 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generates a combined bundle stats comment for GitHub Actions.
|
||||
* Heavily inspired by https://github.com/twk3/rollup-size-compare-action (MIT).
|
||||
*/
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
const REQUIRED_ARGS = new Map([
|
||||
['base', 'Mapping of bundle names to base stats JSON'],
|
||||
['head', 'Mapping of bundle names to head stats JSON'],
|
||||
]);
|
||||
|
||||
function parseRawArgs(argv) {
|
||||
const args = new Map();
|
||||
|
||||
for (let index = 2; index < argv.length; index += 1) {
|
||||
const key = argv[index];
|
||||
|
||||
if (!key?.startsWith('--')) {
|
||||
throw new Error(
|
||||
`Unexpected argument “${key ?? ''}”. Use --key value pairs.`,
|
||||
);
|
||||
}
|
||||
|
||||
const values = [];
|
||||
|
||||
while (index + 1 < argv.length && !argv[index + 1].startsWith('--')) {
|
||||
values.push(argv[index + 1]);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if (values.length === 0) {
|
||||
throw new Error(`Missing value for argument “${key}”.`);
|
||||
}
|
||||
|
||||
const keyName = key.slice(2);
|
||||
// Accumulate values if the key already exists
|
||||
if (args.has(keyName)) {
|
||||
args.set(keyName, [...args.get(keyName), ...values]);
|
||||
} else {
|
||||
args.set(keyName, values);
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function getSingleValue(args, key) {
|
||||
const values = args.get(key);
|
||||
if (!values) {
|
||||
return undefined;
|
||||
}
|
||||
if (values.length !== 1) {
|
||||
throw new Error(`Argument “--${key}” must have exactly one value.`);
|
||||
}
|
||||
return values[0];
|
||||
}
|
||||
|
||||
function parseMapping(values, key, description) {
|
||||
if (!values || values.length === 0) {
|
||||
throw new Error(`Missing required argument “--${key}” (${description}).`);
|
||||
}
|
||||
|
||||
if (values.length === 1) {
|
||||
const [rawValue] = values;
|
||||
const trimmed = rawValue.trim();
|
||||
|
||||
if (trimmed.startsWith('{')) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error('Value must be a JSON object.');
|
||||
}
|
||||
|
||||
return new Map(
|
||||
Object.entries(parsed).map(([name, pathValue]) => {
|
||||
if (typeof pathValue !== 'string') {
|
||||
throw new Error(
|
||||
`Value for “${name}” in “--${key}” must be a string path.`,
|
||||
);
|
||||
}
|
||||
return [name, pathValue];
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Unknown parsing error';
|
||||
throw new Error(
|
||||
`Failed to parse “--${key}” value as JSON object: ${message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const entries = new Map();
|
||||
|
||||
for (const value of values) {
|
||||
const [rawName, ...rawPathParts] = value.split('=');
|
||||
|
||||
if (!rawName || rawPathParts.length === 0) {
|
||||
throw new Error(
|
||||
`Argument “--${key}” must be provided as name=path pairs or a JSON object.`,
|
||||
);
|
||||
}
|
||||
|
||||
const name = rawName.trim();
|
||||
const pathValue = rawPathParts.join('=').trim();
|
||||
|
||||
if (!name) {
|
||||
throw new Error(`Argument “--${key}” contains an empty bundle name.`);
|
||||
}
|
||||
|
||||
if (!pathValue) {
|
||||
throw new Error(
|
||||
`Argument “--${key}” for bundle “${name}” must include a non-empty path.`,
|
||||
);
|
||||
}
|
||||
|
||||
entries.set(name, pathValue);
|
||||
}
|
||||
|
||||
if (entries.size === 0) {
|
||||
throw new Error(`Argument “--${key}” must define at least one bundle.`);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = parseRawArgs(argv);
|
||||
|
||||
const baseMap = parseMapping(
|
||||
args.get('base'),
|
||||
'base',
|
||||
REQUIRED_ARGS.get('base'),
|
||||
);
|
||||
const headMap = parseMapping(
|
||||
args.get('head'),
|
||||
'head',
|
||||
REQUIRED_ARGS.get('head'),
|
||||
);
|
||||
|
||||
const sections = [];
|
||||
|
||||
for (const [name, basePath] of baseMap.entries()) {
|
||||
const headPath = headMap.get(name);
|
||||
|
||||
if (!headPath) {
|
||||
throw new Error(
|
||||
`Bundle “${name}” is missing a corresponding “--head” entry.`,
|
||||
);
|
||||
}
|
||||
|
||||
sections.push({
|
||||
name,
|
||||
basePath,
|
||||
headPath,
|
||||
});
|
||||
}
|
||||
|
||||
for (const name of headMap.keys()) {
|
||||
if (!baseMap.has(name)) {
|
||||
throw new Error(
|
||||
`Bundle “${name}” is missing a corresponding “--base” entry.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sections,
|
||||
identifier: getSingleValue(args, 'identifier') ?? 'bundle-stats',
|
||||
};
|
||||
}
|
||||
|
||||
async function loadStats(filePath) {
|
||||
try {
|
||||
const absolutePath = path.resolve(process.cwd(), filePath);
|
||||
const fileContents = await readFile(absolutePath, 'utf8');
|
||||
const parsed = JSON.parse(fileContents);
|
||||
|
||||
// Validate that we got a meaningful stats object
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
throw new Error('Stats file does not contain a valid JSON object');
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error while parsing stats file';
|
||||
console.error(`[bundle-stats] Failed to parse “${filePath}”: ${message}`);
|
||||
throw new Error(`Failed to load stats file “${filePath}”: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function findAllChildren(node = {}) {
|
||||
if (Array.isArray(node.children)) {
|
||||
return node.children.flatMap(findAllChildren);
|
||||
}
|
||||
return [node];
|
||||
}
|
||||
|
||||
function trimPath(input) {
|
||||
if (!input) {
|
||||
return '';
|
||||
}
|
||||
return input.replace(/.*node_modules/, '/node_modules');
|
||||
}
|
||||
|
||||
function assetNameToSizeMap(statAssets = {}) {
|
||||
const children = statAssets?.tree?.children;
|
||||
|
||||
if (!Array.isArray(children) || children.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
return new Map(
|
||||
children.map(asset => {
|
||||
const descendants = findAllChildren(asset);
|
||||
let size = 0;
|
||||
let gzipSize = statAssets?.options?.gzip ? 0 : null;
|
||||
|
||||
for (const mod of descendants) {
|
||||
const nodePart = statAssets?.nodeParts?.[mod.uid];
|
||||
|
||||
if (!nodePart) {
|
||||
continue;
|
||||
}
|
||||
|
||||
size += nodePart.renderedLength ?? 0;
|
||||
|
||||
if (gzipSize !== null) {
|
||||
gzipSize += nodePart.gzipLength ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
return [trimPath(asset.name), { size, gzipSize }];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function chunkModuleNameToSizeMap(statChunks = {}) {
|
||||
if (!statChunks?.tree) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
return new Map(
|
||||
findAllChildren(statChunks.tree).map(mod => {
|
||||
const modInfo = statChunks?.nodeParts?.[mod.uid] ?? {};
|
||||
const meta = statChunks?.nodeMetas?.[modInfo.metaUid] ?? {};
|
||||
const id = trimPath(meta.id ?? '');
|
||||
|
||||
return [
|
||||
id,
|
||||
{
|
||||
size: modInfo.renderedLength ?? 0,
|
||||
gzipSize: statChunks?.options?.gzip
|
||||
? (modInfo.gzipLength ?? 0)
|
||||
: null,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function sortDiffDescending(items) {
|
||||
return items.sort((a, b) => Math.abs(b.diff) - Math.abs(a.diff));
|
||||
}
|
||||
|
||||
function normaliseGzip(value) {
|
||||
if (value == null || Number.isNaN(value)) {
|
||||
return NaN;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getAssetDiff(name, oldSize, newSize) {
|
||||
const diff = newSize.size - oldSize.size;
|
||||
|
||||
const percent =
|
||||
oldSize.size === 0
|
||||
? newSize.size === 0
|
||||
? 0
|
||||
: Infinity
|
||||
: +((1 - newSize.size / oldSize.size) * -100).toFixed(5) || 0;
|
||||
|
||||
return {
|
||||
name,
|
||||
new: {
|
||||
size: newSize.size,
|
||||
gzipSize: normaliseGzip(newSize.gzipSize),
|
||||
},
|
||||
old: {
|
||||
size: oldSize.size,
|
||||
gzipSize: normaliseGzip(oldSize.gzipSize),
|
||||
},
|
||||
diff,
|
||||
diffPercentage: percent,
|
||||
};
|
||||
}
|
||||
|
||||
function webpackStatsDiff(oldAssets, newAssets) {
|
||||
const added = [];
|
||||
const removed = [];
|
||||
const bigger = [];
|
||||
const smaller = [];
|
||||
const unchanged = [];
|
||||
|
||||
let newSizeTotal = 0;
|
||||
let oldSizeTotal = 0;
|
||||
let newGzipSizeTotal = 0;
|
||||
let oldGzipSizeTotal = 0;
|
||||
|
||||
for (const [name, oldAssetSizes] of oldAssets) {
|
||||
oldSizeTotal += oldAssetSizes.size;
|
||||
oldGzipSizeTotal += oldAssetSizes.gzipSize ?? NaN;
|
||||
|
||||
const newAsset = newAssets.get(name);
|
||||
|
||||
if (!newAsset) {
|
||||
removed.push(getAssetDiff(name, oldAssetSizes, { size: 0, gzipSize: 0 }));
|
||||
continue;
|
||||
}
|
||||
|
||||
const diff = getAssetDiff(name, oldAssetSizes, newAsset);
|
||||
|
||||
if (diff.diffPercentage > 0) {
|
||||
bigger.push(diff);
|
||||
} else if (diff.diffPercentage < 0) {
|
||||
smaller.push(diff);
|
||||
} else {
|
||||
unchanged.push(diff);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, newAssetSizes] of newAssets) {
|
||||
newSizeTotal += newAssetSizes.size;
|
||||
newGzipSizeTotal += newAssetSizes.gzipSize ?? NaN;
|
||||
|
||||
if (!oldAssets.has(name)) {
|
||||
added.push(getAssetDiff(name, { size: 0, gzipSize: 0 }, newAssetSizes));
|
||||
}
|
||||
}
|
||||
|
||||
const oldFilesCount = oldAssets.size;
|
||||
const newFilesCount = newAssets.size;
|
||||
|
||||
return {
|
||||
added: sortDiffDescending(added),
|
||||
removed: sortDiffDescending(removed),
|
||||
bigger: sortDiffDescending(bigger),
|
||||
smaller: sortDiffDescending(smaller),
|
||||
unchanged,
|
||||
total: getAssetDiff(
|
||||
oldFilesCount === newFilesCount
|
||||
? `${newFilesCount}`
|
||||
: `${oldFilesCount} → ${newFilesCount}`,
|
||||
{ size: oldSizeTotal, gzipSize: oldGzipSizeTotal },
|
||||
{ size: newSizeTotal, gzipSize: newGzipSizeTotal },
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function getStatsDiff(oldStats, newStats) {
|
||||
return webpackStatsDiff(
|
||||
assetNameToSizeMap(oldStats),
|
||||
assetNameToSizeMap(newStats),
|
||||
);
|
||||
}
|
||||
|
||||
function getChunkModuleDiff(oldStats, newStats) {
|
||||
const diff = webpackStatsDiff(
|
||||
chunkModuleNameToSizeMap(oldStats),
|
||||
chunkModuleNameToSizeMap(newStats),
|
||||
);
|
||||
|
||||
if (
|
||||
diff.added.length === 0 &&
|
||||
diff.removed.length === 0 &&
|
||||
diff.bigger.length === 0 &&
|
||||
diff.smaller.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
const BYTES_PER_KILOBYTE = 1024;
|
||||
const FILE_SIZE_DENOMINATIONS = [
|
||||
'B',
|
||||
'kB',
|
||||
'MB',
|
||||
'GB',
|
||||
'TB',
|
||||
'PB',
|
||||
'EB',
|
||||
'ZB',
|
||||
'YB',
|
||||
'BB',
|
||||
];
|
||||
|
||||
function formatFileSizeIEC(bytes, precision = 2) {
|
||||
if (bytes == null || Number.isNaN(bytes)) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
if (bytes === 0) {
|
||||
return `0 ${FILE_SIZE_DENOMINATIONS[0]}`;
|
||||
}
|
||||
|
||||
const absBytes = Math.abs(bytes);
|
||||
const denominationIndex = Math.floor(
|
||||
Math.log(absBytes) / Math.log(BYTES_PER_KILOBYTE),
|
||||
);
|
||||
const value = absBytes / Math.pow(BYTES_PER_KILOBYTE, denominationIndex);
|
||||
const stripped = parseFloat(value.toFixed(precision));
|
||||
|
||||
return `${stripped} ${FILE_SIZE_DENOMINATIONS[denominationIndex]}`;
|
||||
}
|
||||
|
||||
function conditionalPercentage(number) {
|
||||
if (number === Infinity || number === -Infinity) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const absValue = Math.abs(number);
|
||||
|
||||
if (absValue === 0 || absValue === 100) {
|
||||
return `${number}%`;
|
||||
}
|
||||
|
||||
const value = Number.isFinite(absValue) ? absValue.toFixed(2) : absValue;
|
||||
return `${signFor(number)}${value}%`;
|
||||
}
|
||||
|
||||
function capitalize(text) {
|
||||
if (!text) return '';
|
||||
return `${text[0].toUpperCase()}${text.slice(1)}`;
|
||||
}
|
||||
|
||||
function makeHeader(columns) {
|
||||
const header = columns.join(' | ');
|
||||
const separator = columns
|
||||
.map(column =>
|
||||
Array.from({ length: column.length })
|
||||
.map(() => '-')
|
||||
.join(''),
|
||||
)
|
||||
.join(' | ');
|
||||
|
||||
return `${header}\n${separator}`;
|
||||
}
|
||||
|
||||
const TOTAL_HEADERS = makeHeader([
|
||||
'Files count',
|
||||
'Total bundle size',
|
||||
'% Changed',
|
||||
]);
|
||||
const TABLE_HEADERS = makeHeader(['Asset', 'File Size', '% Changed']);
|
||||
const CHUNK_TABLE_HEADERS = makeHeader(['File', 'Δ', 'Size']);
|
||||
|
||||
function signFor(num) {
|
||||
if (num === 0) return '';
|
||||
return num > 0 ? '+' : '-';
|
||||
}
|
||||
|
||||
function toFileSizeDiff(oldSize, newSize, diff) {
|
||||
const diffLine = [
|
||||
`${formatFileSizeIEC(oldSize)} → ${formatFileSizeIEC(newSize)}`,
|
||||
];
|
||||
|
||||
if (typeof diff !== 'undefined') {
|
||||
diffLine.push(`(${signFor(diff)}${formatFileSizeIEC(diff)})`);
|
||||
}
|
||||
|
||||
return diffLine.join(' ');
|
||||
}
|
||||
|
||||
function toFileSizeDiffCell(asset) {
|
||||
const lines = [];
|
||||
|
||||
if (asset.diff === 0) {
|
||||
lines.push(formatFileSizeIEC(asset.new.size));
|
||||
|
||||
if (asset.new.gzipSize) {
|
||||
lines.push(formatFileSizeIEC(asset.new.gzipSize));
|
||||
}
|
||||
} else {
|
||||
lines.push(toFileSizeDiff(asset.old.size, asset.new.size, asset.diff));
|
||||
|
||||
if (asset.old.gzipSize || asset.new.gzipSize) {
|
||||
lines.push(
|
||||
`${toFileSizeDiff(asset.old.gzipSize, asset.new.gzipSize)} (gzip)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('<br />');
|
||||
}
|
||||
|
||||
function printAssetTableRow(asset) {
|
||||
return [
|
||||
asset.name,
|
||||
toFileSizeDiffCell(asset),
|
||||
conditionalPercentage(asset.diffPercentage),
|
||||
].join(' | ');
|
||||
}
|
||||
|
||||
function printAssetTablesByGroup(statsDiff) {
|
||||
const statsFields = ['added', 'removed', 'bigger', 'smaller', 'unchanged'];
|
||||
|
||||
return statsFields
|
||||
.map(field => {
|
||||
const assets = statsDiff[field] ?? [];
|
||||
|
||||
if (assets.length === 0) {
|
||||
return `**${capitalize(field)}**\nNo assets were ${field}`;
|
||||
}
|
||||
|
||||
return `**${capitalize(field)}**\n${TABLE_HEADERS}\n${assets
|
||||
.map(asset => printAssetTableRow(asset))
|
||||
.join('\n')}`;
|
||||
})
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
function getDiffEmoji(diff) {
|
||||
if (diff.diffPercentage === Infinity) return '🆕';
|
||||
if (diff.diffPercentage <= -100) return '🔥';
|
||||
if (diff.diffPercentage > 0) return '📈';
|
||||
if (diff.diffPercentage < 0) return '📉';
|
||||
return ' ';
|
||||
}
|
||||
|
||||
function getTrimmedChunkName(chunkModule) {
|
||||
const chunkName = chunkModule.name ?? '';
|
||||
if (chunkName.startsWith('./')) {
|
||||
return chunkName.substring(2);
|
||||
}
|
||||
if (chunkName.startsWith('/')) {
|
||||
return chunkName.substring(1);
|
||||
}
|
||||
return chunkName;
|
||||
}
|
||||
|
||||
function printChunkModuleRow(chunkModule) {
|
||||
const emoji = getDiffEmoji(chunkModule);
|
||||
const chunkName = getTrimmedChunkName(chunkModule);
|
||||
const diffPart = `${chunkModule.diff >= 0 ? '+' : '-'}${formatFileSizeIEC(chunkModule.diff)}`;
|
||||
const percentPart = Number.isFinite(chunkModule.diffPercentage)
|
||||
? ` (${conditionalPercentage(chunkModule.diffPercentage)})`
|
||||
: '';
|
||||
|
||||
return [
|
||||
`\`${chunkName}\``,
|
||||
`${emoji} ${diffPart}${percentPart}`,
|
||||
`${formatFileSizeIEC(chunkModule.old.size)} → ${formatFileSizeIEC(chunkModule.new.size)}`,
|
||||
].join(' | ');
|
||||
}
|
||||
|
||||
function printChunkModulesTable(statsDiff) {
|
||||
if (!statsDiff) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const changedModules = [
|
||||
...(statsDiff.added ?? []),
|
||||
...(statsDiff.removed ?? []),
|
||||
...(statsDiff.bigger ?? []),
|
||||
...(statsDiff.smaller ?? []),
|
||||
].sort((a, b) => b.diffPercentage - a.diffPercentage);
|
||||
|
||||
if (changedModules.length === 0) {
|
||||
return `<details>\n<summary>Changeset</summary>\nNo files were changed\n</details>`;
|
||||
}
|
||||
|
||||
const rows = changedModules
|
||||
.slice(0, 100)
|
||||
.map(chunkModule => printChunkModuleRow(chunkModule))
|
||||
.join('\n');
|
||||
|
||||
const summarySuffix =
|
||||
changedModules.length > 100 ? ' (largest 100 files by percent change)' : '';
|
||||
|
||||
return `<details>\n<summary>Changeset${summarySuffix}</summary>\n\n${CHUNK_TABLE_HEADERS}\n${rows}\n</details>`;
|
||||
}
|
||||
|
||||
function printTotalAssetTable(statsDiff) {
|
||||
return `**Total**\n${TOTAL_HEADERS}\n${printAssetTableRow(statsDiff.total)}`;
|
||||
}
|
||||
|
||||
function renderSection(title, statsDiff, chunkModuleDiff) {
|
||||
const { total, ...groups } = statsDiff;
|
||||
const parts = [`#### ${title}`, '', printTotalAssetTable({ total })];
|
||||
|
||||
const chunkTable = printChunkModulesTable(chunkModuleDiff);
|
||||
if (chunkTable) {
|
||||
parts.push('', chunkTable);
|
||||
}
|
||||
|
||||
parts.push(
|
||||
'',
|
||||
`<details>\n<summary>View detailed bundle breakdown</summary>\n<div>\n\n${printAssetTablesByGroup(
|
||||
groups,
|
||||
)}\n</div>\n</details>`,
|
||||
);
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
|
||||
console.error(
|
||||
`[bundle-stats] Found ${args.sections.length} sections to process`,
|
||||
);
|
||||
args.sections.forEach((section, index) => {
|
||||
console.error(
|
||||
`[bundle-stats] Section ${index + 1}: ${section.name} (base: ${section.basePath}, head: ${section.headPath})`,
|
||||
);
|
||||
});
|
||||
|
||||
const sections = [];
|
||||
|
||||
for (const section of args.sections) {
|
||||
console.error(`[bundle-stats] Processing section: ${section.name}`);
|
||||
console.error(
|
||||
`[bundle-stats] Loading base stats from: ${section.basePath}`,
|
||||
);
|
||||
const baseStats = await loadStats(section.basePath);
|
||||
console.error(
|
||||
`[bundle-stats] Loading head stats from: ${section.headPath}`,
|
||||
);
|
||||
const headStats = await loadStats(section.headPath);
|
||||
|
||||
const statsDiff = getStatsDiff(baseStats, headStats);
|
||||
const chunkDiff = getChunkModuleDiff(baseStats, headStats);
|
||||
|
||||
console.error(
|
||||
`[bundle-stats] Section ${section.name}: ${statsDiff.total.name} files, total size ${statsDiff.total.old.size} → ${statsDiff.total.new.size}`,
|
||||
);
|
||||
|
||||
sections.push({
|
||||
name: section.name,
|
||||
statsDiff,
|
||||
chunkDiff,
|
||||
});
|
||||
}
|
||||
|
||||
const identifier = `<!--- bundlestats-action-comment key:${args.identifier} --->`;
|
||||
|
||||
const comment = [
|
||||
'### Bundle Stats',
|
||||
'',
|
||||
sections
|
||||
.map(section =>
|
||||
renderSection(section.name, section.statsDiff, section.chunkDiff),
|
||||
)
|
||||
.join('\n\n---\n\n'),
|
||||
'',
|
||||
identifier,
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
process.stdout.write(comment);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
151
packages/ci-actions/bin/update-bundle-stats-comment.mjs
Normal file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Updates (or creates) a bundle stats comment on a pull request.
|
||||
* Requires the following environment variables to be set:
|
||||
* - GITHUB_TOKEN
|
||||
* - GITHUB_REPOSITORY (owner/repo)
|
||||
* - PR_NUMBER
|
||||
*/
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
commentFile: null,
|
||||
identifier: null,
|
||||
};
|
||||
|
||||
for (let i = 2; i < argv.length; i += 2) {
|
||||
const key = argv[i];
|
||||
const value = argv[i + 1];
|
||||
|
||||
if (!key?.startsWith('--')) {
|
||||
throw new Error(
|
||||
`Unexpected argument “${key ?? ''}”. Use --key value pairs.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
throw new Error(`Missing value for argument “${key}”.`);
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case '--comment-file':
|
||||
args.commentFile = value;
|
||||
break;
|
||||
case '--identifier':
|
||||
args.identifier = value;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown argument “${key}”.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.commentFile) {
|
||||
throw new Error('Missing required argument “--comment-file“.');
|
||||
}
|
||||
|
||||
if (!args.identifier) {
|
||||
throw new Error('Missing required argument “--identifier“.');
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
async function loadCommentBody(commentFile) {
|
||||
const absolutePath = path.resolve(process.cwd(), commentFile);
|
||||
return readFile(absolutePath, 'utf8');
|
||||
}
|
||||
|
||||
function getRepoInfo() {
|
||||
const repository = process.env.GITHUB_REPOSITORY;
|
||||
if (!repository) {
|
||||
throw new Error('GITHUB_REPOSITORY environment variable is required.');
|
||||
}
|
||||
|
||||
const [owner, repo] = repository.split('/');
|
||||
if (!owner || !repo) {
|
||||
throw new Error(`Invalid GITHUB_REPOSITORY value “${repository}”.`);
|
||||
}
|
||||
|
||||
return { owner, repo };
|
||||
}
|
||||
|
||||
function getPullRequestNumber() {
|
||||
const rawNumber = process.env.PR_NUMBER ?? '';
|
||||
const prNumber = Number.parseInt(rawNumber, 10);
|
||||
|
||||
if (!Number.isInteger(prNumber) || prNumber <= 0) {
|
||||
throw new Error(
|
||||
'PR_NUMBER environment variable must be a positive integer.',
|
||||
);
|
||||
}
|
||||
|
||||
return prNumber;
|
||||
}
|
||||
|
||||
function assertGitHubToken() {
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
if (!token) {
|
||||
throw new Error('GITHUB_TOKEN environment variable is required.');
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
async function listComments(octokit, owner, repo, issueNumber) {
|
||||
return octokit.paginate(octokit.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
}
|
||||
|
||||
function isGitHubActionsBot(comment) {
|
||||
return comment.user?.login === 'github-actions[bot]';
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { commentFile, identifier } = parseArgs(process.argv);
|
||||
const commentBody = await loadCommentBody(commentFile);
|
||||
const token = assertGitHubToken();
|
||||
const { owner, repo } = getRepoInfo();
|
||||
const issueNumber = getPullRequestNumber();
|
||||
|
||||
const octokit = new Octokit({ auth: token });
|
||||
|
||||
const comments = await listComments(octokit, owner, repo, issueNumber);
|
||||
|
||||
const existingComment = comments.find(
|
||||
comment =>
|
||||
isGitHubActionsBot(comment) && comment.body?.includes(identifier),
|
||||
);
|
||||
|
||||
if (existingComment) {
|
||||
await octokit.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existingComment.id,
|
||||
body: commentBody,
|
||||
});
|
||||
console.log('Updated existing bundle stats comment.');
|
||||
} else {
|
||||
await octokit.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
body: commentBody,
|
||||
});
|
||||
console.log('Created new bundle stats comment.');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.9"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest --run"
|
||||
|
||||
@@ -5,10 +5,7 @@ export default defineConfig({
|
||||
globals: true,
|
||||
include: ['src/**/*.test.(js|jsx|ts|tsx)'],
|
||||
environment: 'node',
|
||||
poolOptions: {
|
||||
threads: {
|
||||
singleThread: true,
|
||||
},
|
||||
},
|
||||
maxWorkers: 1,
|
||||
isolate: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react": "^19.2.5",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.9"
|
||||
},
|
||||
"exports": {
|
||||
"./hooks/*": "./src/hooks/*.ts",
|
||||
@@ -40,7 +40,6 @@
|
||||
"./popover": "./src/Popover.tsx",
|
||||
"./select": "./src/Select.tsx",
|
||||
"./space-between": "./src/SpaceBetween.tsx",
|
||||
"./stack": "./src/Stack.tsx",
|
||||
"./styles": "./src/styles.ts",
|
||||
"./text": "./src/Text.tsx",
|
||||
"./text-one-line": "./src/TextOneLine.tsx",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isValidElement,
|
||||
type ReactElement,
|
||||
Ref,
|
||||
RefObject,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
@@ -11,15 +12,20 @@ import {
|
||||
type InitialFocusProps<T extends HTMLElement> = {
|
||||
/**
|
||||
* The child element to focus when the component mounts. This can be either a single React element or a function that returns a React element.
|
||||
* The child element should have a `ref` prop for this to work. For child components which receives a ref via another prop
|
||||
* e.g. `inputRef`, use a function as child and pass the ref to the appropriate prop.
|
||||
*/
|
||||
children: ReactElement<{ ref: Ref<T> }> | ((ref: Ref<T>) => ReactElement);
|
||||
children:
|
||||
| ReactElement<{ ref: Ref<T> }>
|
||||
| ((ref: RefObject<T | null>) => ReactElement);
|
||||
};
|
||||
|
||||
/**
|
||||
* InitialFocus sets focus on its child element
|
||||
* when it mounts.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {ReactElement | function} props.children - A single React element or a function that returns a React element.
|
||||
* @param {ReactElement | function} children - A single React element or a function that returns a React element.
|
||||
* The child element should have a `ref` prop for this to work. For child components which receives a ref via another prop
|
||||
* e.g. `inputRef`, use a function as child and pass the ref to the appropriate prop.
|
||||
*/
|
||||
export function InitialFocus<T extends HTMLElement = HTMLElement>({
|
||||
children,
|
||||
|
||||
@@ -34,6 +34,7 @@ export const Popover = ({
|
||||
|
||||
return (
|
||||
<ReactAriaPopover
|
||||
data-popover={true}
|
||||
ref={ref}
|
||||
placement="bottom end"
|
||||
offset={1}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { View } from './View';
|
||||
type SpaceBetweenProps = {
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
gap?: number;
|
||||
wrap?: boolean;
|
||||
align?: 'start' | 'center' | 'end' | 'stretch';
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
};
|
||||
@@ -13,18 +15,22 @@ type SpaceBetweenProps = {
|
||||
export const SpaceBetween = ({
|
||||
direction = 'horizontal',
|
||||
gap = 15,
|
||||
wrap = true,
|
||||
align = 'center',
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: SpaceBetweenProps) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexWrap: 'wrap',
|
||||
flexWrap: wrap ? 'wrap' : 'nowrap',
|
||||
flexDirection: direction === 'horizontal' ? 'row' : 'column',
|
||||
alignItems: 'center',
|
||||
alignItems: align,
|
||||
gap,
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
// @ts-strict-ignore
|
||||
import React, {
|
||||
Children,
|
||||
type ComponentProps,
|
||||
Fragment,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
type ReactNode,
|
||||
type CSSProperties,
|
||||
} from 'react';
|
||||
|
||||
import { Text } from './Text';
|
||||
import { View } from './View';
|
||||
|
||||
function getChildren(key, children) {
|
||||
return Children.toArray(children).reduce(
|
||||
(list, child) => {
|
||||
if (child) {
|
||||
if (
|
||||
typeof child === 'object' &&
|
||||
'type' in child &&
|
||||
child.type === Fragment
|
||||
) {
|
||||
return list.concat(
|
||||
getChildren(
|
||||
child.key,
|
||||
typeof child.props === 'object' && 'children' in child.props
|
||||
? child.props.children
|
||||
: [],
|
||||
),
|
||||
);
|
||||
}
|
||||
list.push({ key: key + child['key'], child });
|
||||
return list;
|
||||
}
|
||||
return list;
|
||||
},
|
||||
[] as Array<{ key: string; child: ReactNode }>,
|
||||
);
|
||||
}
|
||||
|
||||
type StackProps = ComponentProps<typeof View> & {
|
||||
direction?: CSSProperties['flexDirection'];
|
||||
align?: string;
|
||||
justify?: string;
|
||||
spacing?: number;
|
||||
debug?: boolean;
|
||||
};
|
||||
export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
||||
(
|
||||
{
|
||||
direction = 'column',
|
||||
align,
|
||||
justify,
|
||||
spacing = 3,
|
||||
children,
|
||||
debug,
|
||||
style,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const isReversed = direction.endsWith('reverse');
|
||||
const isHorizontal = direction.startsWith('row');
|
||||
const validChildren = getChildren('', children);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: direction,
|
||||
alignItems: align,
|
||||
justifyContent: justify,
|
||||
...style,
|
||||
}}
|
||||
innerRef={ref}
|
||||
{...props}
|
||||
>
|
||||
{validChildren.map(({ key, child }, index) => {
|
||||
const isLastChild = validChildren.length === index + 1;
|
||||
|
||||
let marginProp;
|
||||
if (isHorizontal) {
|
||||
marginProp = isReversed ? 'marginLeft' : 'marginRight';
|
||||
} else {
|
||||
marginProp = isReversed ? 'marginTop' : 'marginBottom';
|
||||
}
|
||||
|
||||
return cloneElement(
|
||||
typeof child === 'string' ? <Text>{child}</Text> : child,
|
||||
{
|
||||
key,
|
||||
style: {
|
||||
...(debug && { borderWidth: 1, borderColor: 'red' }),
|
||||
...(isLastChild ? null : { [marginProp]: spacing * 5 }),
|
||||
...(child.props ? child.props.style : null),
|
||||
},
|
||||
},
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Stack.displayName = 'Stack';
|
||||
@@ -15,12 +15,14 @@ type TooltipProps = Partial<ComponentProps<typeof AriaTooltip>> & {
|
||||
children: ReactNode;
|
||||
content: ReactNode;
|
||||
triggerProps?: Partial<ComponentProps<typeof TooltipTrigger>>;
|
||||
disablePointerEvents?: boolean;
|
||||
};
|
||||
|
||||
export const Tooltip = ({
|
||||
children,
|
||||
content,
|
||||
triggerProps = {},
|
||||
disablePointerEvents = false,
|
||||
...props
|
||||
}: TooltipProps) => {
|
||||
const triggerRef = useRef(null);
|
||||
@@ -69,7 +71,14 @@ export const Tooltip = ({
|
||||
>
|
||||
{children}
|
||||
|
||||
<AriaTooltip triggerRef={triggerRef} style={styles.tooltip} {...props}>
|
||||
<AriaTooltip
|
||||
triggerRef={triggerRef}
|
||||
style={{
|
||||
...styles.tooltip,
|
||||
...(disablePointerEvents && { pointerEvents: 'none' }),
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
</AriaTooltip>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
import path from 'path';
|
||||
|
||||
function indexTemplate(filePaths) {
|
||||
function indexTemplate(filePaths: { path: string }[]) {
|
||||
const exportEntries = filePaths.map(({ path: filePath }) => {
|
||||
const basename = path.basename(filePath, path.extname(filePath));
|
||||
const exportName = `Svg${basename}`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
const tmpl = ({ imports, interfaces, componentName, props, jsx }, { tpl }) => {
|
||||
import { type Config } from '@svgr/core';
|
||||
|
||||
const tmpl: Config['template'] = ({ imports, interfaces, componentName, props, jsx }, { tpl }) => {
|
||||
return tpl`
|
||||
${imports};
|
||||
|
||||
|
||||
@@ -21,12 +21,7 @@ export default defineConfig({
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
include: ['src/**/*.web.test.(js|jsx|ts|tsx)'],
|
||||
poolOptions: {
|
||||
threads: {
|
||||
maxThreads: 2,
|
||||
minThreads: 1,
|
||||
},
|
||||
},
|
||||
maxWorkers: 2,
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google-protobuf": "^3.15.12",
|
||||
"protoc-gen-js": "^3.21.4-4",
|
||||
"ts-protoc-gen": "^0.15.0",
|
||||
"@types/google-protobuf": "3.15.12",
|
||||
"protoc-gen-js": "3.21.4-4",
|
||||
"ts-protoc-gen": "0.15.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["dist"]
|
||||
"exclude": ["dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
|
||||
3
packages/desktop-client/.gitignore
vendored
@@ -31,3 +31,6 @@ public/*.wasm
|
||||
|
||||
# translations
|
||||
locale/
|
||||
|
||||
# service worker build output
|
||||
dev-dist
|
||||
|
||||
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 114 KiB |
@@ -194,47 +194,71 @@ export class AccountPage {
|
||||
transaction: TransactionEntry,
|
||||
) {
|
||||
if (transaction.debit) {
|
||||
// double click to ensure the content is selected when adding split transactions
|
||||
await transactionRow.getByTestId('debit').dblclick();
|
||||
await this.page.keyboard.type(transaction.debit);
|
||||
const debitCell = transactionRow.getByTestId('debit');
|
||||
await debitCell.click();
|
||||
const debitInput = debitCell.getByRole('textbox');
|
||||
await this.selectInputText(debitInput);
|
||||
await debitInput.pressSequentially(transaction.debit);
|
||||
await this.page.keyboard.press('Tab');
|
||||
}
|
||||
|
||||
if (transaction.credit) {
|
||||
await transactionRow.getByTestId('credit').click();
|
||||
await this.page.keyboard.type(transaction.credit);
|
||||
const creditCell = transactionRow.getByTestId('credit');
|
||||
await creditCell.click();
|
||||
const creditInput = creditCell.getByRole('textbox');
|
||||
await this.selectInputText(creditInput);
|
||||
await creditInput.pressSequentially(transaction.credit);
|
||||
await this.page.keyboard.press('Tab');
|
||||
}
|
||||
|
||||
if (transaction.account) {
|
||||
await transactionRow.getByTestId('account').click();
|
||||
await this.page.keyboard.type(transaction.account);
|
||||
const accountCell = transactionRow.getByTestId('account');
|
||||
await accountCell.click();
|
||||
const accountInput = accountCell.getByRole('textbox');
|
||||
await this.selectInputText(accountInput);
|
||||
await accountInput.pressSequentially(transaction.account);
|
||||
await this.page.keyboard.press('Tab');
|
||||
}
|
||||
|
||||
if (transaction.payee) {
|
||||
await transactionRow.getByTestId('payee').click();
|
||||
await this.page.keyboard.type(transaction.payee);
|
||||
const payeeCell = transactionRow.getByTestId('payee');
|
||||
await payeeCell.click();
|
||||
const payeeInput = payeeCell.getByRole('textbox');
|
||||
await this.selectInputText(payeeInput);
|
||||
await payeeInput.pressSequentially(transaction.payee);
|
||||
await this.page.keyboard.press('Tab');
|
||||
}
|
||||
|
||||
if (transaction.notes) {
|
||||
await transactionRow.getByTestId('notes').click();
|
||||
await this.page.keyboard.type(transaction.notes);
|
||||
const notesCell = transactionRow.getByTestId('notes');
|
||||
await notesCell.click();
|
||||
const notesInput = notesCell.getByRole('textbox');
|
||||
await this.selectInputText(notesInput);
|
||||
await notesInput.pressSequentially(transaction.notes);
|
||||
await this.page.keyboard.press('Tab');
|
||||
}
|
||||
|
||||
if (transaction.category) {
|
||||
await transactionRow.getByTestId('category').click();
|
||||
const categoryCell = transactionRow.getByTestId('category');
|
||||
await categoryCell.click();
|
||||
|
||||
if (transaction.category === 'split') {
|
||||
await this.page.getByTestId('split-transaction-button').click();
|
||||
} else {
|
||||
await this.page.keyboard.type(transaction.category);
|
||||
const categoryInput = categoryCell.getByRole('textbox');
|
||||
await this.selectInputText(categoryInput);
|
||||
await categoryInput.pressSequentially(transaction.category);
|
||||
await this.page.keyboard.press('Tab');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async selectInputText(input: Locator) {
|
||||
const value = await input.inputValue();
|
||||
if (value) {
|
||||
await input.selectText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FilterTooltip {
|
||||
|
||||
171
packages/desktop-client/e2e/page-models/edit-rule-modal.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { type Page, type Locator } from '@playwright/test';
|
||||
|
||||
type ConditionsEntry = {
|
||||
field: string;
|
||||
op: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type ActionsEntry = {
|
||||
field: string;
|
||||
op?: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type SplitsEntry = {
|
||||
field: string;
|
||||
op?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
type RuleEntry = {
|
||||
conditionsOp?: string | RegExp;
|
||||
conditions?: ConditionsEntry[];
|
||||
actions?: ActionsEntry[];
|
||||
splits?: Array<SplitsEntry[]>;
|
||||
};
|
||||
|
||||
export class EditRuleModal {
|
||||
readonly page: Page;
|
||||
readonly locator: Locator;
|
||||
readonly heading: Locator;
|
||||
readonly conditionsOpButton: Locator;
|
||||
readonly conditionList: Locator;
|
||||
readonly actionList: Locator;
|
||||
readonly splitIntoMultipleTransactionsButton: Locator;
|
||||
readonly saveButton: Locator;
|
||||
readonly cancelButton: Locator;
|
||||
|
||||
constructor(locator: Locator) {
|
||||
this.locator = locator;
|
||||
this.page = locator.page();
|
||||
|
||||
this.heading = locator.getByRole('heading');
|
||||
this.conditionsOpButton = locator
|
||||
.getByTestId('conditions-op')
|
||||
.getByRole('button');
|
||||
this.conditionList = locator.getByTestId('condition-list');
|
||||
this.actionList = locator.getByTestId('action-list');
|
||||
this.splitIntoMultipleTransactionsButton = locator.getByTestId(
|
||||
'add-split-transactions',
|
||||
);
|
||||
this.saveButton = locator.getByRole('button', { name: 'Save' });
|
||||
this.cancelButton = locator.getByRole('button', { name: 'Cancel' });
|
||||
}
|
||||
|
||||
async fill(data: RuleEntry) {
|
||||
if (data.conditionsOp) {
|
||||
await this.selectConditionsOp(data.conditionsOp);
|
||||
}
|
||||
|
||||
if (data.conditions) {
|
||||
await this.fillEditorFields(data.conditions, this.conditionList, true);
|
||||
}
|
||||
|
||||
if (data.actions) {
|
||||
await this.fillEditorFields(data.actions, this.actionList);
|
||||
}
|
||||
|
||||
if (data.splits) {
|
||||
let idx = data.actions?.length ?? 0;
|
||||
for (const splitActions of data.splits) {
|
||||
await this.splitIntoMultipleTransactionsButton.click();
|
||||
await this.fillEditorFields(splitActions, this.actionList.nth(idx));
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fillEditorFields(
|
||||
data: Array<ConditionsEntry | ActionsEntry | SplitsEntry>,
|
||||
rootElement: Locator,
|
||||
fieldFirst = false,
|
||||
) {
|
||||
for (const [idx, entry] of data.entries()) {
|
||||
const { field, op, value } = entry;
|
||||
|
||||
const row = await this.getRow(rootElement, idx);
|
||||
|
||||
if (!(await row.isVisible())) {
|
||||
await this.addEntry(rootElement);
|
||||
}
|
||||
|
||||
if (op && !fieldFirst) {
|
||||
await this.selectOp(row, op);
|
||||
}
|
||||
|
||||
if (field) {
|
||||
await this.selectField(row, field);
|
||||
}
|
||||
|
||||
if (op && fieldFirst) {
|
||||
await this.selectOp(row, op);
|
||||
}
|
||||
|
||||
if (value && value.length > 0) {
|
||||
const input = row.getByRole('textbox');
|
||||
const existingValue = await input.inputValue();
|
||||
if (existingValue) {
|
||||
await input.selectText();
|
||||
}
|
||||
// Using pressSequentially here to simulate user typing.
|
||||
// When using .fill(...), playwright just "pastes" the entire word onto the input
|
||||
// and for some reason this breaks the autocomplete highlighting logic
|
||||
// e.g. "Create payee" option is not being highlighted.
|
||||
await input.pressSequentially(value);
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async selectConditionsOp(conditionsOp: string | RegExp) {
|
||||
await this.conditionsOpButton.click();
|
||||
|
||||
const conditionsOpSelectOption =
|
||||
await this.getPopoverSelectOption(conditionsOp);
|
||||
await conditionsOpSelectOption.click();
|
||||
}
|
||||
|
||||
async selectOp(row: Locator, op: string) {
|
||||
await row.getByTestId('op-select').getByRole('button').click();
|
||||
|
||||
const opSelectOption = await this.getPopoverSelectOption(op);
|
||||
await opSelectOption.waitFor({ state: 'visible' });
|
||||
await opSelectOption.click();
|
||||
}
|
||||
|
||||
async selectField(row: Locator, field: string) {
|
||||
await row.getByTestId('field-select').getByRole('button').click();
|
||||
|
||||
const fieldSelectOption = await this.getPopoverSelectOption(field);
|
||||
await fieldSelectOption.waitFor({ state: 'visible' });
|
||||
await fieldSelectOption.click();
|
||||
}
|
||||
|
||||
async getRow(locator: Locator, index: number) {
|
||||
return locator.getByTestId('editor-row').nth(index);
|
||||
}
|
||||
|
||||
async addEntry(locator: Locator) {
|
||||
await locator.getByRole('button', { name: 'Add entry' }).click();
|
||||
}
|
||||
|
||||
async getPopoverSelectOption(value: string | RegExp) {
|
||||
// Need to use page because popover is rendered outside of modal locator
|
||||
return this.page
|
||||
.locator('[data-popover]')
|
||||
.getByRole('button', { name: value, exact: true });
|
||||
}
|
||||
|
||||
async save() {
|
||||
await this.saveButton.click();
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
await this.cancelButton.click();
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.heading.getByRole('button', { name: 'Close' }).click();
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export class MobilePayeesPage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on a payee to view/edit rules
|
||||
* Click on a payee to open the edit page
|
||||
*/
|
||||
async clickPayee(index: number) {
|
||||
const payee = this.getNthPayee(index);
|
||||
|
||||
@@ -1,52 +1,26 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
|
||||
type ConditionsEntry = {
|
||||
field: string;
|
||||
op: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type ActionsEntry = {
|
||||
field: string;
|
||||
op?: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type SplitsEntry = {
|
||||
field: string;
|
||||
op?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
type RuleEntry = {
|
||||
conditionsOp?: string | RegExp;
|
||||
conditions?: ConditionsEntry[];
|
||||
actions?: ActionsEntry[];
|
||||
splits?: Array<SplitsEntry[]>;
|
||||
};
|
||||
import { EditRuleModal } from './edit-rule-modal';
|
||||
|
||||
export class RulesPage {
|
||||
readonly page: Page;
|
||||
readonly searchBox: Locator;
|
||||
readonly createNewRuleButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.searchBox = page.getByPlaceholder('Filter rules...');
|
||||
this.createNewRuleButton = page.getByRole('button', {
|
||||
name: 'Create new rule',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new rule
|
||||
* Open the edit rule modal to create a new rule.
|
||||
*/
|
||||
async createRule(data: RuleEntry) {
|
||||
await this.page
|
||||
.getByRole('button', {
|
||||
name: 'Create new rule',
|
||||
})
|
||||
.click();
|
||||
|
||||
await this._fillRuleFields(data);
|
||||
|
||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
||||
async createNewRule() {
|
||||
await this.createNewRuleButton.click();
|
||||
return new EditRuleModal(this.page.getByTestId('edit-rule-modal'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,108 +39,4 @@ export class RulesPage {
|
||||
async searchFor(text: string) {
|
||||
await this.searchBox.fill(text);
|
||||
}
|
||||
|
||||
async _fillRuleFields(data: RuleEntry) {
|
||||
if (data.conditionsOp) {
|
||||
await this.page
|
||||
.getByTestId('conditions-op')
|
||||
.getByRole('button')
|
||||
.first()
|
||||
.click();
|
||||
await this.page
|
||||
.getByRole('button', { exact: true, name: data.conditionsOp })
|
||||
.click();
|
||||
}
|
||||
|
||||
if (data.conditions) {
|
||||
await this._fillEditorFields(
|
||||
data.conditions,
|
||||
this.page.getByTestId('condition-list'),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
if (data.actions) {
|
||||
await this._fillEditorFields(
|
||||
data.actions,
|
||||
this.page.getByTestId('action-list'),
|
||||
);
|
||||
}
|
||||
|
||||
if (data.splits) {
|
||||
let idx = data.actions?.length ?? 0;
|
||||
for (const splitActions of data.splits) {
|
||||
await this.page.getByTestId('add-split-transactions').click();
|
||||
await this._fillEditorFields(
|
||||
splitActions,
|
||||
this.page.getByTestId('action-list').nth(idx),
|
||||
);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _fillEditorFields(
|
||||
data: Array<ConditionsEntry | ActionsEntry | SplitsEntry>,
|
||||
rootElement: Locator,
|
||||
fieldFirst = false,
|
||||
) {
|
||||
for (const [idx, entry] of data.entries()) {
|
||||
const { field, op, value } = entry;
|
||||
|
||||
const row = rootElement.getByTestId('editor-row').nth(idx);
|
||||
|
||||
if (!(await row.isVisible())) {
|
||||
await rootElement.getByRole('button', { name: 'Add entry' }).click();
|
||||
}
|
||||
|
||||
if (op && !fieldFirst) {
|
||||
await row.getByTestId('op-select').getByRole('button').first().click();
|
||||
await this.page
|
||||
.getByRole('button', { name: op, exact: true })
|
||||
.first()
|
||||
.waitFor({ state: 'visible' });
|
||||
|
||||
await this.page
|
||||
.getByRole('button', { name: op, exact: true })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
}
|
||||
|
||||
if (field) {
|
||||
await row
|
||||
.getByTestId('field-select')
|
||||
.getByRole('button')
|
||||
.first()
|
||||
.click();
|
||||
await this.page
|
||||
.getByRole('button', { name: field, exact: true })
|
||||
.first()
|
||||
.waitFor({ state: 'visible' });
|
||||
|
||||
await this.page
|
||||
.getByRole('button', { name: field, exact: true })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
}
|
||||
|
||||
if (op && fieldFirst) {
|
||||
await row.getByTestId('op-select').getByRole('button').first().click();
|
||||
await this.page
|
||||
.getByRole('button', { name: op, exact: true })
|
||||
.first()
|
||||
.waitFor({ state: 'visible' });
|
||||
|
||||
await this.page
|
||||
.getByRole('button', { name: op, exact: true })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
}
|
||||
|
||||
if (value) {
|
||||
await row.getByRole('textbox').fill(value);
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { type Page, type Locator } from '@playwright/test';
|
||||
|
||||
type ScheduleEntry = {
|
||||
scheduleName?: string;
|
||||
payee?: string;
|
||||
account?: string;
|
||||
amount?: number;
|
||||
};
|
||||
|
||||
export class ScheduleEditModal {
|
||||
readonly page: Page;
|
||||
readonly locator: Locator;
|
||||
readonly heading: Locator;
|
||||
readonly scheduleNameInput: Locator;
|
||||
readonly payeeInput: Locator;
|
||||
readonly accountInput: Locator;
|
||||
readonly amountInput: Locator;
|
||||
readonly addButton: Locator;
|
||||
readonly saveButton: Locator;
|
||||
readonly cancelButton: Locator;
|
||||
|
||||
constructor(locator: Locator) {
|
||||
this.locator = locator;
|
||||
this.page = locator.page();
|
||||
|
||||
this.heading = locator.getByRole('heading');
|
||||
this.scheduleNameInput = locator.getByRole('textbox', {
|
||||
name: 'Schedule name',
|
||||
});
|
||||
this.payeeInput = locator.getByRole('textbox', { name: 'Payee' });
|
||||
this.accountInput = locator.getByRole('textbox', { name: 'Account' });
|
||||
this.amountInput = locator.getByLabel('Amount');
|
||||
this.addButton = locator.getByRole('button', { name: 'Add' });
|
||||
this.saveButton = locator.getByRole('button', { name: 'Save' });
|
||||
this.cancelButton = locator.getByRole('button', { name: 'Cancel' });
|
||||
}
|
||||
|
||||
async fill(data: ScheduleEntry) {
|
||||
// Using pressSequentially on autocomplete fields here to simulate user typing.
|
||||
// When using .fill(...), playwright just "pastes" the entire word onto the input
|
||||
// and for some reason this breaks the autocomplete highlighting logic
|
||||
// e.g. "Create payee" option is not being highlighted.
|
||||
|
||||
if (data.scheduleName) {
|
||||
await this.scheduleNameInput.fill(data.scheduleName);
|
||||
}
|
||||
|
||||
if (data.payee) {
|
||||
await this.payeeInput.pressSequentially(data.payee);
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
if (data.account) {
|
||||
await this.accountInput.pressSequentially(data.account);
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
if (data.amount) {
|
||||
await this.amountInput.fill(String(data.amount));
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
await this.saveButton.click();
|
||||
}
|
||||
|
||||
async add() {
|
||||
await this.addButton.click();
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
await this.cancelButton.click();
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.heading.getByRole('button', { name: 'Close' }).click();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
|
||||
type ScheduleEntry = {
|
||||
payee?: string;
|
||||
account?: string;
|
||||
amount?: number;
|
||||
};
|
||||
import { ScheduleEditModal } from './schedule-edit-modal';
|
||||
|
||||
export class SchedulesPage {
|
||||
readonly page: Page;
|
||||
@@ -21,17 +17,12 @@ export class SchedulesPage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new schedule
|
||||
* Open the schedule edit modal.
|
||||
*/
|
||||
async addNewSchedule(data: ScheduleEntry) {
|
||||
async addNewSchedule() {
|
||||
await this.addNewScheduleButton.click();
|
||||
|
||||
await this._fillScheduleFields(data);
|
||||
|
||||
await this.page
|
||||
.getByTestId('schedule-edit-modal')
|
||||
.getByRole('button', { name: 'Add' })
|
||||
.click();
|
||||
return new ScheduleEditModal(this.page.getByTestId('schedule-edit-modal'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,26 +74,4 @@ export class SchedulesPage {
|
||||
await actions.getByRole('button').click();
|
||||
await this.page.getByRole('button', { name: actionName }).click();
|
||||
}
|
||||
|
||||
async _fillScheduleFields(data: ScheduleEntry) {
|
||||
if (data.payee) {
|
||||
await this.page.getByRole('textbox', { name: 'Payee' }).fill(data.payee);
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
if (data.account) {
|
||||
await this.page
|
||||
.getByRole('textbox', { name: 'Account' })
|
||||
.fill(data.account);
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
if (data.amount) {
|
||||
await this.page.getByLabel('Amount').fill(String(data.amount));
|
||||
// For some readon, the input field does not trigger the change event on tests
|
||||
// but it works on the browser. We can revisit this once migration to
|
||||
// react aria components is complete.
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ test.describe('Mobile Payees', () => {
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('clicking on a payee opens rule creation form', async () => {
|
||||
test('clicking on a payee opens payee edit page', async () => {
|
||||
await payeesPage.waitForLoadingToComplete();
|
||||
|
||||
const payeeCount = await payeesPage.getPayeeCount();
|
||||
@@ -70,8 +70,16 @@ test.describe('Mobile Payees', () => {
|
||||
|
||||
await payeesPage.clickPayee(0);
|
||||
|
||||
// Should navigate to rules page for creating a new rule
|
||||
await expect(page).toHaveURL(/\/rules/);
|
||||
// Should navigate to payee edit page
|
||||
await expect(page).toHaveURL(/\/payees\/.+/);
|
||||
|
||||
// Check that the edit page elements are visible
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Edit Payee' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByPlaceholder('Payee name')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible();
|
||||
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 116 KiB |