Compare commits
770
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4cc32340c | ||
|
|
44d4d231be | ||
|
|
7000c95661 | ||
|
|
5c6444e604 | ||
|
|
17ae27fb21 | ||
|
|
5abe08fbab | ||
|
|
3a0d01876f | ||
|
|
0aa4c721c1 | ||
|
|
c71cfabed6 | ||
|
|
7d18e16925 | ||
|
|
2cce621d95 | ||
|
|
2ac2319b55 | ||
|
|
cb3909f052 | ||
|
|
678d7400ed | ||
|
|
ebd2d6a766 | ||
|
|
93b1655ca8 | ||
|
|
5d9783b85e | ||
|
|
48d6217ff7 | ||
|
|
7d2c7c50af | ||
|
|
e285432795 | ||
|
|
5961a3ff5d | ||
|
|
259a3fa8b8 | ||
|
|
37288aada4 | ||
|
|
128c5647ba | ||
|
|
024943141d | ||
|
|
a1ad25bbae | ||
|
|
77d62dd156 | ||
|
|
7d70768c3f | ||
|
|
efc09808c6 | ||
|
|
f004f5fa20 | ||
|
|
963cbf1230 | ||
|
|
a57d0fdb7f | ||
|
|
00a98d2aa7 | ||
|
|
d37fcd09d1 | ||
|
|
c19b48c445 | ||
|
|
0e90a7652b | ||
|
|
4b6cc8ba0f | ||
|
|
2d56129a55 | ||
|
|
fe4b80b0a6 | ||
|
|
be91aebbf3 | ||
|
|
ffa906bf5a | ||
|
|
409865ccc0 | ||
|
|
3381cae267 | ||
|
|
97a8032cd5 | ||
|
|
17724a8018 | ||
|
|
ffd056182f | ||
|
|
4d4c95678f | ||
|
|
6371bfb24a | ||
|
|
e815ac6a27 | ||
|
|
483b685411 | ||
|
|
223f1d3438 | ||
|
|
64e7914900 | ||
|
|
cfb74f87af | ||
|
|
3761f122b8 | ||
|
|
df2e5ac82e | ||
|
|
5fb29fe547 | ||
|
|
71154a8fdc | ||
|
|
b4055cfd86 | ||
|
|
e3dfdd376c | ||
|
|
8d103ac6e3 | ||
|
|
a84ad64c9e | ||
|
|
5e0ff0df95 | ||
|
|
69c8193e2a | ||
|
|
906b981d21 | ||
|
|
9c44e10e02 | ||
|
|
bc55cc7ba0 | ||
|
|
d91645b6ee | ||
|
|
48d93a479f | ||
|
|
dfe492f8df | ||
|
|
418a48bef6 | ||
|
|
7b1fbb1cbb | ||
|
|
049d7671e1 | ||
|
|
4f8fa58c14 | ||
|
|
deb8ed4449 | ||
|
|
90594317f8 | ||
|
|
6be6e98870 | ||
|
|
5cff4dfcb2 | ||
|
|
811a149728 | ||
|
|
e2440ed297 | ||
|
|
075f20f401 | ||
|
|
96f90c9fa2 | ||
|
|
46120ece93 | ||
|
|
fe154888db | ||
|
|
a79eec1006 | ||
|
|
76e9aead49 | ||
|
|
550a9dc382 | ||
|
|
3dcab43846 | ||
|
|
a2c3571672 | ||
|
|
3f781d8024 | ||
|
|
f15abdfd89 | ||
|
|
0de65be033 | ||
|
|
c8f2257abb | ||
|
|
5a82fd1c81 | ||
|
|
03b472525f | ||
|
|
285d290199 | ||
|
|
aa99d2c147 | ||
|
|
2f9d1a0b31 | ||
|
|
fd2fbe3821 | ||
|
|
c050643155 | ||
|
|
7246c2a3d5 | ||
|
|
01f6c85948 | ||
|
|
d7e69bb58c | ||
|
|
27b5c480af | ||
|
|
426670f614 | ||
|
|
32f7994744 | ||
|
|
f51d504a66 | ||
|
|
5bd85a785f | ||
|
|
80adc10c6c | ||
|
|
63b8340e03 | ||
|
|
61835c99de | ||
|
|
8866531afb | ||
|
|
88c82f3f24 | ||
|
|
56e0434e64 | ||
|
|
48018d1fa5 | ||
|
|
3e2be18590 | ||
|
|
274d4f6e0f | ||
|
|
5ef9ebdcac | ||
|
|
fdb491076c | ||
|
|
7980d7b199 | ||
|
|
31b8fdb3ad | ||
|
|
1436a0a210 | ||
|
|
97925fbc84 | ||
|
|
f6b9267b3b | ||
|
|
282bc1c43e | ||
|
|
4466021b04 | ||
|
|
8e1284fff6 | ||
|
|
6711b64f8f | ||
|
|
80d5711617 | ||
|
|
82ee4875ee | ||
|
|
8d4ef2dbcf | ||
|
|
0485d455b7 | ||
|
|
eda220d878 | ||
|
|
9b77934e0e | ||
|
|
d4736cd624 | ||
|
|
1ede739640 | ||
|
|
8fe3683579 | ||
|
|
cdd2593bac | ||
|
|
0ff123bfae | ||
|
|
fd81838909 | ||
|
|
7f2014bb98 | ||
|
|
7d643349b2 | ||
|
|
250edad378 | ||
|
|
577ed75dd0 | ||
|
|
8e5c2d2a8b | ||
|
|
2dfb7779f6 | ||
|
|
de50d8444a | ||
|
|
2c1d7c0346 | ||
|
|
5c4abb5146 | ||
|
|
da8b4dd6f7 | ||
|
|
51c8769998 | ||
|
|
40c38ab69e | ||
|
|
a2b5c4000f | ||
|
|
d00ac79e91 | ||
|
|
184adf1693 | ||
|
|
4d29b46a6c | ||
|
|
5a49a3fab5 | ||
|
|
0c1996bb68 | ||
|
|
5e34b440bb | ||
|
|
adc0be3432 | ||
|
|
cb75bd381d | ||
|
|
f96114a9cc | ||
|
|
bfad81267a | ||
|
|
b7a7ad1068 | ||
|
|
c830be2720 | ||
|
|
3dbba7e478 | ||
|
|
660cbe3940 | ||
|
|
af19ab222b | ||
|
|
6005050023 | ||
|
|
f0ebc8a80b | ||
|
|
29465fb750 | ||
|
|
ea7738975a | ||
|
|
47a3024683 | ||
|
|
f5e1b9fb79 | ||
|
|
db5f9bef51 | ||
|
|
dc2b39bce4 | ||
|
|
eee77d5a38 | ||
|
|
bcdd78683b | ||
|
|
e196eb3c63 | ||
|
|
45fb64fedb | ||
|
|
a701a8dbbe | ||
|
|
0dcdee4637 | ||
|
|
c2c3f0a4ed | ||
|
|
7f77d82c0f | ||
|
|
e8544c4eb5 | ||
|
|
2fecfe548e | ||
|
|
52da7153c7 | ||
|
|
e0eb841a2e | ||
|
|
1e74d370f3 | ||
|
|
dc1950520a | ||
|
|
132199cb9b | ||
|
|
0d8623dc60 | ||
|
|
7abad3786d | ||
|
|
d17caa7524 | ||
|
|
aa143d79b7 | ||
|
|
4f476e49a2 | ||
|
|
731bf88de9 | ||
|
|
5709db91f5 | ||
|
|
a3a72789b5 | ||
|
|
e8e3e28135 | ||
|
|
1f60ba7b20 | ||
|
|
49b3c621e8 | ||
|
|
8f1bdfb257 | ||
|
|
547a54d097 | ||
|
|
7f7a7f2fb3 | ||
|
|
cc43782285 | ||
|
|
6eb4ad1a85 | ||
|
|
e4b00e594c | ||
|
|
29e0309986 | ||
|
|
31b10abd75 | ||
|
|
45ae938a58 | ||
|
|
4d6b627a13 | ||
|
|
caac8448be | ||
|
|
f50a8e0ca4 | ||
|
|
b398111b0e | ||
|
|
29e03f68a2 | ||
|
|
919b084b93 | ||
|
|
7228983fc2 | ||
|
|
4bf72efc0a | ||
|
|
62f3df2378 | ||
|
|
d41fb09903 | ||
|
|
1a94606fc4 | ||
|
|
3dc749e4f2 | ||
|
|
97bcb320ab | ||
|
|
f234bfffc6 | ||
|
|
0059492899 | ||
|
|
0cbcd890fd | ||
|
|
e2bc7586e2 | ||
|
|
532ca3a08f | ||
|
|
ab7d6102fa | ||
|
|
3dba6036ae | ||
|
|
7552b8be49 | ||
|
|
d84c720e60 | ||
|
|
a251ed58c2 | ||
|
|
3de5f0af4b | ||
|
|
13ed2e9f8d | ||
|
|
7fe542cde7 | ||
|
|
d87686ca4f | ||
|
|
e7da505280 | ||
|
|
6e27936545 | ||
|
|
e4a3b6adf3 | ||
|
|
61bf0c26a5 | ||
|
|
93c13910e3 | ||
|
|
2ff4dda480 | ||
|
|
ee001efd34 | ||
|
|
21f3c4a8c4 | ||
|
|
7c646625c2 | ||
|
|
3a6c921c73 | ||
|
|
31644221e6 | ||
|
|
4d0c217634 | ||
|
|
8168e33032 | ||
|
|
c62e9d70fe | ||
|
|
f6f0da2a98 | ||
|
|
e01943a86e | ||
|
|
592c85c476 | ||
|
|
b07c6605c5 | ||
|
|
08bfc45b7e | ||
|
|
4aca3bdbc7 | ||
|
|
0c91711a9e | ||
|
|
3a70d3279c | ||
|
|
9d05b3fb06 | ||
|
|
6df9953d8f | ||
|
|
41e6307d03 | ||
|
|
1299a82d79 | ||
|
|
fc1e037490 | ||
|
|
bc78d15bcd | ||
|
|
b85235928c | ||
|
|
9d3d374058 | ||
|
|
5edd81d9f2 | ||
|
|
88629b46b5 | ||
|
|
dcee15a58e | ||
|
|
a692f96467 | ||
|
|
539f27b1d1 | ||
|
|
f7d4e94540 | ||
|
|
09ef472bcb | ||
|
|
e841c87036 | ||
|
|
83f85a4051 | ||
|
|
ab053e8872 | ||
|
|
c3fa2ba65d | ||
|
|
e70090513c | ||
|
|
cfb8f9a403 | ||
|
|
23bfb75f07 | ||
|
|
b1f2d77ef8 | ||
|
|
d9748c6a15 | ||
|
|
15e96abac5 | ||
|
|
8b4c4a4051 | ||
|
|
8f80d46049 | ||
|
|
f3a96c195b | ||
|
|
67dd0f7e6b | ||
|
|
cd1ffe0f7e | ||
|
|
020b28dd69 | ||
|
|
71abb9f0aa | ||
|
|
61bc72d006 | ||
|
|
bbb5c32854 | ||
|
|
134f2facc5 | ||
|
|
d12fa816b7 | ||
|
|
7ed92ad039 | ||
|
|
d62c3d6840 | ||
|
|
4e58d1c77f | ||
|
|
c48f02942a | ||
|
|
587b8c3226 | ||
|
|
7d7a7542bf | ||
|
|
f94fd56cea | ||
|
|
865aaa3f65 | ||
|
|
d6be5b09b3 | ||
|
|
fb2ade61fc | ||
|
|
0712513342 | ||
|
|
0a65168b09 | ||
|
|
00fac82ad3 | ||
|
|
6a2561fa7a | ||
|
|
ef6bf03935 | ||
|
|
baf6df3e80 | ||
|
|
33ab3d2bf8 | ||
|
|
b27a8c609e | ||
|
|
584fa5732e | ||
|
|
fc2a8aeed0 | ||
|
|
a090edeb7f | ||
|
|
73d8b1e84e | ||
|
|
e4521c1bbd | ||
|
|
d61e51af75 | ||
|
|
4a2224c680 | ||
|
|
14609a71a2 | ||
|
|
d7906a4b1a | ||
|
|
0a16d437cc | ||
|
|
b44166a9bc | ||
|
|
ed6a3869c7 | ||
|
|
a1549d8724 | ||
|
|
fd78ec0e14 | ||
|
|
c330ee34a1 | ||
|
|
9719f061e6 | ||
|
|
04dd3e8b68 | ||
|
|
4c3f31e972 | ||
|
|
619b36b3a6 | ||
|
|
cda21f1d62 | ||
|
|
93b1d62ef0 | ||
|
|
c902687518 | ||
|
|
90fee672ce | ||
|
|
2a47ad775b | ||
|
|
3b62326ced | ||
|
|
aa64ec9a4f | ||
|
|
fe0cf953d7 | ||
|
|
94ce4e5a46 | ||
|
|
6fddfa268e | ||
|
|
3f2395db7c | ||
|
|
e25f641757 | ||
|
|
59732a3e6f | ||
|
|
d37ce2f897 | ||
|
|
09784fcc92 | ||
|
|
3aece780f2 | ||
|
|
26f47a021f | ||
|
|
1e1d524305 | ||
|
|
947eeea0dd | ||
|
|
1cd0ffcef2 | ||
|
|
c1fcff4dec | ||
|
|
ee8431a661 | ||
|
|
19976635bc | ||
|
|
01b6ac90eb | ||
|
|
21cb201d83 | ||
|
|
8fcea0cdf3 | ||
|
|
7721248925 | ||
|
|
d2d79ce8fd | ||
|
|
e0959b06f5 | ||
|
|
4012fb1699 | ||
|
|
e768c4b1ca | ||
|
|
74500ac10b | ||
|
|
45585daeaa | ||
|
|
7043dc72d3 | ||
|
|
83e877a2bc | ||
|
|
aa2b28d611 | ||
|
|
ccaab9aad5 | ||
|
|
18db532910 | ||
|
|
46597aa24e | ||
|
|
e40f5a5da9 | ||
|
|
dbc6b07b62 | ||
|
|
9543da587c | ||
|
|
7ce9c9230f | ||
|
|
b3e781868f | ||
|
|
7db7133ea2 | ||
|
|
23394639cc | ||
|
|
87eb95bf66 | ||
|
|
36d785fbc6 | ||
|
|
c7aa553537 | ||
|
|
ecef7e791b | ||
|
|
90ef25ca62 | ||
|
|
b850ae3132 | ||
|
|
ef22991e6f | ||
|
|
94b7a853a2 | ||
|
|
b2e3469827 | ||
|
|
3efb5c8e2a | ||
|
|
ec81434684 | ||
|
|
cb8aae545e | ||
|
|
016491b41c | ||
|
|
bc7ea4d3ef | ||
|
|
5e98f9d810 | ||
|
|
1b3cc7703d | ||
|
|
e35af28dac | ||
|
|
42493fa731 | ||
|
|
1c0ea5be84 | ||
|
|
955ebc0127 | ||
|
|
c74c708f35 | ||
|
|
b7706e8f62 | ||
|
|
5164cc2a44 | ||
|
|
cb59806455 | ||
|
|
4b60e4cd95 | ||
|
|
8919d9f83e | ||
|
|
f0d18a8f7c | ||
|
|
451280ace1 | ||
|
|
2c22669d65 | ||
|
|
30e862935e | ||
|
|
124b067694 | ||
|
|
30af71af24 | ||
|
|
ad328b725d | ||
|
|
bb80d2c4f6 | ||
|
|
eea7ce6ba6 | ||
|
|
dc82479210 | ||
|
|
ca3433c3f1 | ||
|
|
54d17b9e43 | ||
|
|
534f007d6b | ||
|
|
2e01fabfd6 | ||
|
|
3553c690a6 | ||
|
|
31e23bc538 | ||
|
|
158133f15a | ||
|
|
bda2a070f4 | ||
|
|
3628915c5d | ||
|
|
cce11a6917 | ||
|
|
49c44db179 | ||
|
|
72e8b2b3bb | ||
|
|
a1a32908c5 | ||
|
|
e2d1f4f387 | ||
|
|
54ce26ccb8 | ||
|
|
65ad77a63d | ||
|
|
85a89659dc | ||
|
|
0a1ae34275 | ||
|
|
6afd935759 | ||
|
|
34257e0820 | ||
|
|
f2805a5a39 | ||
|
|
7bd8f5de1c | ||
|
|
3024302725 | ||
|
|
c779955714 | ||
|
|
2294bfa19c | ||
|
|
b7f44e4744 | ||
|
|
00685ab3e7 | ||
|
|
02f63ae1f2 | ||
|
|
29c9561a3f | ||
|
|
fbfbda4eca | ||
|
|
722d65daff | ||
|
|
3d208b340a | ||
|
|
868f2a1e18 | ||
|
|
81db8fa9a1 | ||
|
|
2c3fbe38cb | ||
|
|
fae6eaec1b | ||
|
|
bcb80f08ee | ||
|
|
da4e1b55a8 | ||
|
|
0db6153420 | ||
|
|
e751da36fd | ||
|
|
ddeb75aa4f | ||
|
|
4fb4dafa1d | ||
|
|
9013b97fcc | ||
|
|
509c24b8c2 | ||
|
|
5580f21725 | ||
|
|
ae0e7407e3 | ||
|
|
16087f19b4 | ||
|
|
efff828746 | ||
|
|
5cdaca6048 | ||
|
|
8411b99fc4 | ||
|
|
7a3f00b924 | ||
|
|
a24c56cd52 | ||
|
|
4717cfec4e | ||
|
|
16ce7ef109 | ||
|
|
54685a55a1 | ||
|
|
25862ce7bb | ||
|
|
902f6d3da9 | ||
|
|
f1c8da41f5 | ||
|
|
a035d841aa | ||
|
|
3260b21deb | ||
|
|
290793fdcc | ||
|
|
5a701be9d1 | ||
|
|
537c377fa5 | ||
|
|
b422d37fa2 | ||
|
|
d7fe983681 | ||
|
|
5cccebaa17 | ||
|
|
f7577b59a6 | ||
|
|
686c599f53 | ||
|
|
03ef2e3ef7 | ||
|
|
18433c2499 | ||
|
|
ab5a4f9c5e | ||
|
|
e2cea36daf | ||
|
|
326cd6c7a0 | ||
|
|
3e0cba033a | ||
|
|
1f025695d5 | ||
|
|
164e4a4f05 | ||
|
|
afd75bc571 | ||
|
|
259aa68129 | ||
|
|
750eb611f5 | ||
|
|
5434d7c6c6 | ||
|
|
1beee4695b | ||
|
|
1bce595733 | ||
|
|
afcd481422 | ||
|
|
ce33f28414 | ||
|
|
72b7998935 | ||
|
|
99b8005e73 | ||
|
|
a38c04c84a | ||
|
|
d06bf4660a | ||
|
|
8b30e302b3 | ||
|
|
1b8e0f3845 | ||
|
|
52cba6cd95 | ||
|
|
c2f2345f0d | ||
|
|
1f65553df3 | ||
|
|
f5c9441888 | ||
|
|
16b1a730f6 | ||
|
|
203b7340ab | ||
|
|
39722554db | ||
|
|
6a6e170fe4 | ||
|
|
56eced522e | ||
|
|
b836c23037 | ||
|
|
6ea162d2a4 | ||
|
|
10a451abc7 | ||
|
|
b8cdc38609 | ||
|
|
97871759bb | ||
|
|
42f3afd142 | ||
|
|
e95c6f37de | ||
|
|
1f5b7a0a30 | ||
|
|
b2176d5cab | ||
|
|
e68ded7f19 | ||
|
|
b9a3863787 | ||
|
|
f868f3c767 | ||
|
|
6c9bdbd0a2 | ||
|
|
3c671736ec | ||
|
|
46306a12eb | ||
|
|
5fe2367366 | ||
|
|
ea69034287 | ||
|
|
edc411ae4d | ||
|
|
b27f3fe62e | ||
|
|
eeb137e055 | ||
|
|
5b8ef2617a | ||
|
|
7275574e77 | ||
|
|
35bb9efba4 | ||
|
|
977e1c57ea | ||
|
|
f5b29f16e3 | ||
|
|
4a5696c134 | ||
|
|
55c564968c | ||
|
|
9807177337 | ||
|
|
e15d4aa61e | ||
|
|
1a40508b48 | ||
|
|
c17a46e404 | ||
|
|
5371afbbb3 | ||
|
|
34accaef7f | ||
|
|
dee4caf7bc | ||
|
|
267d84c9e2 | ||
|
|
113ff62459 | ||
|
|
2c7912fe59 | ||
|
|
9d5c3eea19 | ||
|
|
6064a14d47 | ||
|
|
73d6337824 | ||
|
|
afd8bcd754 | ||
|
|
6877d75e87 | ||
|
|
97d2e864e3 | ||
|
|
60cb8ea9f9 | ||
|
|
3c3df29faa | ||
|
|
d783f46f5d | ||
|
|
cc143f7352 | ||
|
|
b65e187e78 | ||
|
|
55f14524df | ||
|
|
191cd775cd | ||
|
|
b34de5c29e | ||
|
|
f26a60aef7 | ||
|
|
8b43589dc5 | ||
|
|
f7af6cceff | ||
|
|
7b11c97603 | ||
|
|
a01153595b | ||
|
|
770244c0e7 | ||
|
|
61c8f08aea | ||
|
|
de4880081d | ||
|
|
eb001edc2e | ||
|
|
d96b950901 | ||
|
|
c345656307 | ||
|
|
0f8a61e973 | ||
|
|
3626590e94 | ||
|
|
81122dc2df | ||
|
|
3f734e2f8c | ||
|
|
0022e278c6 | ||
|
|
2463ac2cf4 | ||
|
|
0f2bc3f627 | ||
|
|
603663a5a4 | ||
|
|
39fe0b7858 | ||
|
|
be237a2901 | ||
|
|
54e8defdc6 | ||
|
|
c8799259ea | ||
|
|
28da766fba | ||
|
|
3bfc327f8f | ||
|
|
c095aa4ccc | ||
|
|
bce3b368e7 | ||
|
|
462a6e404d | ||
|
|
21c0630334 | ||
|
|
6970a5da89 | ||
|
|
d1a130ba84 | ||
|
|
85e91947f6 | ||
|
|
76bd68b2aa | ||
|
|
56adc00bb9 | ||
|
|
eec9dcef03 | ||
|
|
5501e168f7 | ||
|
|
db5f3791c5 | ||
|
|
19ef4b560f | ||
|
|
ecb967aa35 | ||
|
|
427720b02c | ||
|
|
fa2010ae63 | ||
|
|
5083e02722 | ||
|
|
82363cbd9d | ||
|
|
fbd7a4e63a | ||
|
|
364d76e46b | ||
|
|
f711dbb111 | ||
|
|
b108406a42 | ||
|
|
8a24c16b6e | ||
|
|
92c9924515 | ||
|
|
c98a58be34 | ||
|
|
1b770cc72b | ||
|
|
953bc61515 | ||
|
|
3d745a1c0b | ||
|
|
5a80694a56 | ||
|
|
4b5aac67d3 | ||
|
|
a42c2e2976 | ||
|
|
1961dd61b3 | ||
|
|
c31ef469c5 | ||
|
|
add11b04f6 | ||
|
|
c02a0f2166 | ||
|
|
6ce22c3670 | ||
|
|
d2d1fd79ed | ||
|
|
959fa52508 | ||
|
|
aa0eb88d8c | ||
|
|
d5a2f30de7 | ||
|
|
8eb8427b0a | ||
|
|
cef906d03a | ||
|
|
ccecdbde56 | ||
|
|
be95483566 | ||
|
|
09b5f48136 | ||
|
|
f538ae7fa6 | ||
|
|
623a6216e7 | ||
|
|
9779715f16 | ||
|
|
c3b7a8bc4e | ||
|
|
a44e2d2e49 | ||
|
|
4023356d3f | ||
|
|
60a6b0f0c7 | ||
|
|
72a628aead | ||
|
|
fb675306e0 | ||
|
|
fc3226ff94 | ||
|
|
5902ed9596 | ||
|
|
ff6264f633 | ||
|
|
abd7606301 | ||
|
|
9c7f00351c | ||
|
|
b3c1ef8605 | ||
|
|
63692b18b3 | ||
|
|
0b51b8ac8b | ||
|
|
3eba5f7510 | ||
|
|
2b2e880eed | ||
|
|
2b15c13e86 | ||
|
|
3015e9f90e | ||
|
|
63f9583d47 | ||
|
|
a0f23f35e5 | ||
|
|
18d1d01a13 | ||
|
|
8b3ac7de1a | ||
|
|
a190c0e6b4 | ||
|
|
44607ecfe7 | ||
|
|
6d56c03c7a | ||
|
|
b7f71ec9e2 | ||
|
|
626542be59 | ||
|
|
f69f6ce364 | ||
|
|
bf842f5a6f | ||
|
|
0c2fa676f6 | ||
|
|
2fb9ab2ba9 | ||
|
|
9588b28cf6 | ||
|
|
3356e8c04c | ||
|
|
ea9f615884 | ||
|
|
a5d95385ca | ||
|
|
0518902619 | ||
|
|
6dc21c7763 | ||
|
|
ca63e47ecd | ||
|
|
2a3b3b9c36 | ||
|
|
29c621f835 | ||
|
|
b36bfce109 | ||
|
|
34c12d99c7 | ||
|
|
beb164a9a2 | ||
|
|
cb85e6d289 | ||
|
|
397cf6f527 | ||
|
|
908d99ad80 | ||
|
|
2cfa93bfdc | ||
|
|
377ca9cdfd | ||
|
|
06fe54a774 | ||
|
|
707b5e6c51 | ||
|
|
351f188d02 | ||
|
|
f4adf308f9 | ||
|
|
2b1edd47b2 | ||
|
|
68a4a6cc22 | ||
|
|
01e1a8de0f | ||
|
|
341299f477 | ||
|
|
b81d678b3f | ||
|
|
d673d67a1c | ||
|
|
d57a9955d5 | ||
|
|
cb7a1a8868 | ||
|
|
2944f5e802 | ||
|
|
3682eaf718 | ||
|
|
5d656b0b8a | ||
|
|
fe0a3da325 | ||
|
|
7397d1fbfd | ||
|
|
bceab72f8c | ||
|
|
ca99c24b01 | ||
|
|
de57b5dfc2 | ||
|
|
520dd5c3bb | ||
|
|
0b67dbab8e | ||
|
|
9e9acb071a | ||
|
|
76687fcc82 | ||
|
|
b677f50358 | ||
|
|
91843ab099 | ||
|
|
069ffc8c3a | ||
|
|
32bc673863 | ||
|
|
fe409baf00 | ||
|
|
8ac12ec7d6 | ||
|
|
d9127d7124 | ||
|
|
1fa158942a | ||
|
|
99234a4fbb | ||
|
|
f620e9ee6b | ||
|
|
55bc67834b | ||
|
|
7860c4b2e4 | ||
|
|
b89770c1da | ||
|
|
d9705d0e2f | ||
|
|
c3c1f8fbab | ||
|
|
a2e29a9463 | ||
|
|
18f11555b6 | ||
|
|
0d803dd825 | ||
|
|
7f9726c00e | ||
|
|
05a4119b43 | ||
|
|
d7d79c20a1 | ||
|
|
0d94ae5b38 | ||
|
|
1dfa9be45b | ||
|
|
96f3a194e1 | ||
|
|
89e6e4d1cc | ||
|
|
3a78ac6eb4 | ||
|
|
fc27de8a2e | ||
|
|
dcb8211050 | ||
|
|
09e41d7d88 | ||
|
|
45b077387e | ||
|
|
66f86213fb | ||
|
|
48e60a6539 | ||
|
|
1e6cde3916 | ||
|
|
59d557ad5e | ||
|
|
191e1e7db5 | ||
|
|
4ed5f14629 | ||
|
|
b66905a437 | ||
|
|
d562ecb388 | ||
|
|
b0cba5bf8b | ||
|
|
22e908d831 | ||
|
|
c08171e544 | ||
|
|
6774df1001 | ||
|
|
8550bd3124 | ||
|
|
1bf35283bc | ||
|
|
a99c58d282 | ||
|
|
a3d150e2b0 | ||
|
|
2fad766013 | ||
|
|
c6e34f7949 | ||
|
|
a42235e1a5 | ||
|
|
899d5030ce | ||
|
|
eace5f13a9 | ||
|
|
9995058806 | ||
|
|
e934aa9ee2 | ||
|
|
68bdaff26e | ||
|
|
02e8df179e | ||
|
|
e5fe2690ce | ||
|
|
eda9236c67 | ||
|
|
3b96ebad44 | ||
|
|
63b57a773b | ||
|
|
86d5dd3035 | ||
|
|
aad293a22d |
No files matched your search
@@ -0,0 +1,27 @@
|
||||
# Trust list for PR builds.
|
||||
#
|
||||
# A pull request only builds packages (and spins up builder droplets) when
|
||||
# its author is trusted: repository collaborators are trusted automatically
|
||||
# and do not need listing; external contributors listed here are trusted
|
||||
# too. Anyone else gets the plan only, until a maintainer either adds them
|
||||
# here or applies the "build-approved" label to that one PR. The label also
|
||||
# releases GitHub's approval hold for that PR's build and test workflows.
|
||||
# It remains effective while attached, without vouching for the author's
|
||||
# other PRs. An explicit denouncement cannot be overridden by the label.
|
||||
#
|
||||
# Syntax:
|
||||
# github:username
|
||||
# -github:username reason for denouncement
|
||||
#
|
||||
# Keep entries sorted alphabetically.
|
||||
github:bjarneo
|
||||
github:DanWahlin
|
||||
github:dhh
|
||||
github:f-trycua
|
||||
github:HANCORE-linux
|
||||
github:kwilczynski
|
||||
github:ryanrhughes
|
||||
github:scottjones
|
||||
github:spencerbull
|
||||
github:tcballard
|
||||
github:tobi
|
||||
@@ -0,0 +1,88 @@
|
||||
const BUILD = '.github/workflows/build-pr.yml';
|
||||
const TESTS = '.github/workflows/test.yml';
|
||||
|
||||
// pullRequest/action/since default to the pull_request_target event. The
|
||||
// sync workflows pass them explicitly: GitHub creates no pull_request_target
|
||||
// run for a GITHUB_TOKEN push, so they release their own pushes' held runs.
|
||||
module.exports = async function approve({ github, context, core, vouchStatus,
|
||||
pullRequest = context.payload.pull_request, action = context.payload.action, since,
|
||||
sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), attempts = 36 }) {
|
||||
// Missing/failed vouch lookups must not become approval. Denouncements
|
||||
// remain absolute, just as they are in the package build gate.
|
||||
if (!['unknown', 'bot', 'collaborator', 'vouched'].includes(vouchStatus)) {
|
||||
throw new Error(`Cannot approve workflows: vouch status is ${vouchStatus || 'missing'}.`);
|
||||
}
|
||||
|
||||
const expected = pullRequest;
|
||||
const eventTime = Date.parse(since ?? expected.updated_at);
|
||||
if (!Number.isFinite(eventTime)) throw new Error('Missing PR event timestamp.');
|
||||
const approved = new Set();
|
||||
let precedingBuild;
|
||||
|
||||
const stillApproved = async () => {
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
...context.repo, pull_number: expected.number,
|
||||
});
|
||||
return pr.state === 'open' && pr.head.sha === expected.head.sha &&
|
||||
pr.labels.some(label => label.name === 'build-approved');
|
||||
};
|
||||
|
||||
// The label and PR-run events arrive independently. Wait for the build
|
||||
// belonging to this event, rather than returning after approving an older
|
||||
// run and leaving the new label-triggered run stuck behind GitHub's gate.
|
||||
for (let attempt = 0; attempt < attempts; attempt++) {
|
||||
if (attempt) await sleep(5000);
|
||||
if (!await stillApproved()) {
|
||||
core.info('PR closed, head changed, or build-approved removed; stopping.');
|
||||
return;
|
||||
}
|
||||
|
||||
const all = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, {
|
||||
...context.repo, event: 'pull_request', head_sha: expected.head.sha, per_page: 100,
|
||||
});
|
||||
const runs = all.filter(run =>
|
||||
run.event === 'pull_request' && run.head_sha === expected.head.sha &&
|
||||
run.head_repository?.id === expected.head.repo.id && run.head_branch === expected.head.ref &&
|
||||
[BUILD, TESTS].includes(run.path) &&
|
||||
// Fork runs awaiting approval often have no pull_requests entries.
|
||||
(!run.pull_requests?.length || run.pull_requests.some(pr => pr.number === expected.number))
|
||||
).sort((a, b) => a.id - b.id);
|
||||
|
||||
const newestBuild = runs.findLast(run => run.path === BUILD);
|
||||
if (!newestBuild || !(Date.parse(newestBuild.created_at) >= eventTime) ||
|
||||
!runs.some(run => run.path === TESTS &&
|
||||
(action === 'labeled' || Date.parse(run.created_at) >= eventTime))) continue;
|
||||
|
||||
if (precedingBuild) {
|
||||
const { data: run } = await github.rest.actions.getWorkflowRun({
|
||||
...context.repo, run_id: precedingBuild,
|
||||
});
|
||||
// Approve older builds first, and let them acquire concurrency before
|
||||
// releasing a newer build. Otherwise an older queued run could start
|
||||
// last and cancel the label-triggered build that carries approval.
|
||||
if (!['in_progress', 'completed'].includes(run.status) || run.conclusion === 'action_required') continue;
|
||||
precedingBuild = undefined;
|
||||
}
|
||||
|
||||
const pending = runs.filter(run => run.conclusion === 'action_required' && !approved.has(run.id) &&
|
||||
// If the newest build already runs (e.g. a maintainer approved it),
|
||||
// don't resurrect an obsolete hold that could cancel that newer run.
|
||||
(run.path !== BUILD || run.id === newestBuild.id || newestBuild.conclusion === 'action_required'));
|
||||
if (!pending.length) return;
|
||||
const run = pending[0];
|
||||
// Recheck after the API reads, immediately before exercising write access.
|
||||
if (!await stillApproved()) return;
|
||||
await github.rest.actions.approveWorkflowRun({ ...context.repo, run_id: run.id });
|
||||
approved.add(run.id);
|
||||
core.info(`Approved ${run.path} run ${run.id} for PR #${expected.number}.`);
|
||||
// Only a newer held build needs this one to take the concurrency slot
|
||||
// first. A lone build may sit pending behind an in-flight build of an
|
||||
// older commit (sync branches queue rather than cancel); waiting for it
|
||||
// to start would time out before the tests run was released.
|
||||
if (run.path === BUILD && pending.some(other => other.path === BUILD && other.id > run.id)) {
|
||||
precedingBuild = run.id;
|
||||
}
|
||||
if (pending.length === 1) return;
|
||||
}
|
||||
throw new Error('Timed out waiting for PR workflows. Remove and reapply build-approved to retry.');
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
const approvePrWorkflows = require('./approve-pr-workflows.cjs');
|
||||
|
||||
const BOT = 'github-actions[bot]';
|
||||
|
||||
// A sync workflow pushes its branch with GITHUB_TOKEN. GitHub holds the
|
||||
// resulting pull_request runs for approval and, unlike a person's push,
|
||||
// creates no pull_request_target run, so approve-pr.yml never sees it. The
|
||||
// sync workflow therefore releases the runs for the commit it just pushed,
|
||||
// under the same rule approve-pr.yml applies: only while build-approved is
|
||||
// on the PR. The sync applies that label itself, so its pushes build without
|
||||
// a maintainer. It acts only on its own bot-authored, same-repository PR for
|
||||
// the branch and commit it pushed.
|
||||
module.exports = async function approveSyncPush({ github, context, core,
|
||||
number, branch, headSha, since, approve = approvePrWorkflows, ...options }) {
|
||||
if (!Number.isInteger(number) || !branch || !headSha || !since) {
|
||||
throw new Error('Missing sync PR number, branch, head SHA or push time.');
|
||||
}
|
||||
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: number });
|
||||
const repository = `${context.repo.owner}/${context.repo.repo}`;
|
||||
if (pr.user?.login !== BOT || pr.head.repo?.full_name !== repository ||
|
||||
pr.base.repo?.full_name !== repository || pr.head.ref !== branch) {
|
||||
throw new Error(`PR #${number} is not ${BOT}'s ${branch} PR in ${repository}; refusing to approve.`);
|
||||
}
|
||||
if (pr.state !== 'open' || pr.head.sha !== headSha) {
|
||||
core.info(`PR #${number} is closed or has moved past ${headSha}; nothing to approve.`);
|
||||
return;
|
||||
}
|
||||
if (!pr.labels.some(label => label.name === 'build-approved')) {
|
||||
core.info(`PR #${number} has no build-approved label; its runs stay held.`);
|
||||
return;
|
||||
}
|
||||
await approve({ github, context, core, vouchStatus: 'bot', pullRequest: pr,
|
||||
action: 'synchronize', since, ...options });
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
// Whether a PR rides to master on its own once the required checks pass.
|
||||
//
|
||||
// The rule is the build gate's, from build-pr.yml: a PR trusted to build is
|
||||
// trusted to ship. Collaborators, vouched authors and bots are trusted; an
|
||||
// unknown author is trusted while the PR carries build-approved; a
|
||||
// denouncement is absolute. Two limits on top of it:
|
||||
//
|
||||
// - Only package changes. A PR's workflows, scripts and build tooling never
|
||||
// run in its own build (build-pr.yml overlays only its package directories
|
||||
// onto base tooling), so green checks say nothing about them, and after
|
||||
// merge they run with the publish secrets. Allowed: anything under
|
||||
// pkgbuilds/, plus the test a package PR brings with it, which is a new
|
||||
// file under tests/ and lines in test.yml that only run new tests/*.sh.
|
||||
// test.yml runs on PRs only; an existing test may also run on master
|
||||
// (builder-images.yml runs tests/build-isolation.sh with packages: write),
|
||||
// so editing one still needs a maintainer.
|
||||
// - Not the upstream sync. It labels its own PR build-approved to release
|
||||
// GitHub's hold on its pushes, which is no one's approval; it stays on the
|
||||
// reviewed lane.
|
||||
const PACKAGES = 'pkgbuilds/';
|
||||
const TESTS = 'tests/';
|
||||
const TEST_WORKFLOW = '.github/workflows/test.yml';
|
||||
const TEST_LINE = /^\+\s*\.\/tests\/[A-Za-z0-9._-]+\.sh\s*$/;
|
||||
|
||||
// Every changed line adds a ./tests/<name>.sh call; nothing removed. A diff
|
||||
// too large for the API has no patch and does not qualify.
|
||||
function onlyRunsTests(patch) {
|
||||
if (!patch) return false;
|
||||
const changed = patch.split('\n').filter(line =>
|
||||
/^[+-]/.test(line) && !line.startsWith('+++') && !line.startsWith('---'));
|
||||
return changed.length > 0 && changed.every(line => TEST_LINE.test(line));
|
||||
}
|
||||
|
||||
function packageChange(file) {
|
||||
if (file.previous_filename && !file.previous_filename.startsWith(PACKAGES)) return false;
|
||||
if (file.filename.startsWith(PACKAGES)) return true;
|
||||
if (file.filename.startsWith(TESTS)) return file.status === 'added';
|
||||
if (file.filename === TEST_WORKFLOW) return file.status === 'modified' && onlyRunsTests(file.patch);
|
||||
return false;
|
||||
}
|
||||
const REVIEWED_BRANCHES = /^auto\/sync-upstream(\/|$)/;
|
||||
|
||||
function decide({ pr, files, vouchStatus, repository }) {
|
||||
if (pr.state !== 'open') return { enable: false, reason: 'PR is not open' };
|
||||
if (pr.draft) return { enable: false, reason: 'PR is a draft' };
|
||||
|
||||
const labelled = pr.labels.some(label => label.name === 'build-approved');
|
||||
let trusted;
|
||||
switch (vouchStatus) {
|
||||
case 'bot': case 'collaborator': case 'vouched': trusted = true; break;
|
||||
case 'unknown': trusted = labelled; break;
|
||||
default: trusted = false; // denounced, or a failed lookup
|
||||
}
|
||||
if (!trusted) {
|
||||
return { enable: false, reason: `author not trusted to build (${vouchStatus || 'missing'}${labelled ? ', labelled' : ''})` };
|
||||
}
|
||||
|
||||
if (pr.head.repo?.full_name === repository && REVIEWED_BRANCHES.test(pr.head.ref)) {
|
||||
return { enable: false, reason: `${pr.head.ref} stays on the reviewed lane` };
|
||||
}
|
||||
|
||||
if (!files.length) return { enable: false, reason: 'PR changes no files' };
|
||||
const outside = files.filter(file => !packageChange(file));
|
||||
if (outside.length) {
|
||||
const names = outside.slice(0, 3).map(file => file.filename).join(', ');
|
||||
return { enable: false, reason: `changes more than packages and their new tests: ${names}${outside.length > 3 ? ', ...' : ''}` };
|
||||
}
|
||||
return { enable: true, reason: `${vouchStatus === 'unknown' ? 'build-approved' : vouchStatus} author, package changes only` };
|
||||
}
|
||||
|
||||
module.exports = async function autoMerge({ github, context, core, number, vouchStatus }) {
|
||||
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number: number });
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
...context.repo, pull_number: number, per_page: 100,
|
||||
});
|
||||
const decision = decide({
|
||||
pr, files, vouchStatus, repository: `${context.repo.owner}/${context.repo.repo}`,
|
||||
});
|
||||
core.info(`#${number}: ${decision.enable ? 'auto-merge' : 'leave for a maintainer'} (${decision.reason})`);
|
||||
core.setOutput('enable', String(decision.enable));
|
||||
core.setOutput('head_sha', pr.head.sha);
|
||||
return decision;
|
||||
};
|
||||
module.exports.decide = decide;
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# Usage: sync-pr-branch.sh BASE_BRANCH [PACKAGE...]
|
||||
#
|
||||
# Prints the branch a sync workflow run pushes to, as branch=/scope= lines for
|
||||
# $GITHUB_OUTPUT. An unscoped (scheduled) run owns BASE_BRANCH and regenerates
|
||||
# it from master every time. A run scoped to named packages regenerates only
|
||||
# those, so it gets its own branch and PR: pushing it to BASE_BRANCH would
|
||||
# replace every other pending update there with just the named packages.
|
||||
set -euo pipefail
|
||||
|
||||
base=${1:?base branch required}
|
||||
shift
|
||||
if (( $# == 0 )); then
|
||||
printf 'branch=%s\nscope=\n' "$base"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
names=()
|
||||
for name in "$@"; do
|
||||
# Package directory names, as pacman allows them. Anything else is a typo
|
||||
# or an attempt to smuggle something into a ref name or PR title.
|
||||
if [[ ! $name =~ ^[a-z0-9@_+][a-z0-9@._+-]*$ ]]; then
|
||||
echo "invalid package name: $name" >&2
|
||||
exit 1
|
||||
fi
|
||||
names+=("$name")
|
||||
done
|
||||
mapfile -t names < <(printf '%s\n' "${names[@]}" | sort -u)
|
||||
|
||||
scope="${names[*]}"
|
||||
slug=$(printf '%s\n' "${names[@]}" | sed 's/[^a-z0-9]\{1,\}/-/g; s/^-//; s/-$//' | paste -sd- -)
|
||||
# Keep long package lists to a readable ref; the hash keeps distinct lists apart.
|
||||
hash=$(printf '%s' "$scope" | sha256sum | cut -c1-10)
|
||||
if [[ -z $slug ]]; then
|
||||
slug=$hash
|
||||
elif (( ${#slug} > 60 )); then
|
||||
slug="${slug:0:48}"
|
||||
slug="${slug%-}-$hash"
|
||||
fi
|
||||
printf 'branch=%s-%s\nscope=%s\n' "$base" "$slug" "$scope"
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Approve PR workflows
|
||||
|
||||
# A pull_request workflow cannot approve itself: GitHub can hold it before
|
||||
# any job starts. This workflow only runs trusted default-branch code and
|
||||
# releases the ordinary, unprivileged PR workflows after build approval.
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: write
|
||||
|
||||
concurrency:
|
||||
group: approve-pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
approve:
|
||||
# Match build-pr.yml's events, including other labels applied while this
|
||||
# PR still carries build-approved: each labeled event creates a build.
|
||||
if: contains(github.event.pull_request.labels.*.name, 'build-approved')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
# Never check out the PR head or its merge ref with this write token.
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
persist-credentials: false
|
||||
- id: vouch
|
||||
uses: mitchellh/vouch/action/check-user@f23dbb5e745334f97414ec70463ce7301071a661 # v1
|
||||
with:
|
||||
user: ${{ github.event.pull_request.user.login }}
|
||||
allow-fail: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Approve this PR's pending build and test runs
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
VOUCH_STATUS: ${{ steps.vouch.outputs.status }}
|
||||
with:
|
||||
script: |
|
||||
const approve = require('./.github/scripts/approve-pr-workflows.cjs');
|
||||
await approve({ github, context, core, vouchStatus: process.env.VOUCH_STATUS });
|
||||
@@ -0,0 +1,108 @@
|
||||
name: Auto-merge approved package PRs
|
||||
|
||||
# A package PR trusted to build is trusted to ship: once its builds are
|
||||
# green it should merge and publish without a maintainer pressing the
|
||||
# button. This enables GitHub's auto-merge on such PRs; branch protection
|
||||
# still holds the merge until `result`, `self-tests` and `build-isolation`
|
||||
# pass, and a red build stays an open PR. .github/scripts/auto-merge-pr.cjs
|
||||
# has the rule.
|
||||
#
|
||||
# Auto-merge is enabled with the PAT in PKGS_BOT_TOKEN: a merge made with the
|
||||
# built-in GITHUB_TOKEN does not start publish.yml.
|
||||
#
|
||||
# pull_request_target runs this default-branch code with secrets; the PR's
|
||||
# code is never checked out here.
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review, labeled, unlabeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr:
|
||||
description: 'PR number to evaluate'
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: auto-merge-pr-${{ github.event.pull_request.number || github.event.inputs.pr }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
auto-merge:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Find the PR's author
|
||||
id: pr
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr }}
|
||||
with:
|
||||
script: |
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
...context.repo, pull_number: Number(process.env.NUMBER),
|
||||
});
|
||||
core.setOutput('number', String(pr.number));
|
||||
core.setOutput('author', pr.user.login);
|
||||
|
||||
- id: vouch
|
||||
uses: mitchellh/vouch/action/check-user@f23dbb5e745334f97414ec70463ce7301071a661 # v1
|
||||
with:
|
||||
user: ${{ steps.pr.outputs.author }}
|
||||
allow-fail: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Decide
|
||||
id: decide
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
NUMBER: ${{ steps.pr.outputs.number }}
|
||||
VOUCH_STATUS: ${{ steps.vouch.outputs.status }}
|
||||
with:
|
||||
script: |
|
||||
const autoMerge = require('./.github/scripts/auto-merge-pr.cjs');
|
||||
await autoMerge({ github, context, core,
|
||||
number: Number(process.env.NUMBER), vouchStatus: process.env.VOUCH_STATUS });
|
||||
|
||||
- name: Enable auto-merge
|
||||
if: steps.decide.outputs.enable == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||
PR: ${{ steps.pr.outputs.number }}
|
||||
HEAD_SHA: ${{ steps.decide.outputs.head_sha }}
|
||||
run: |
|
||||
if [[ -z "$GH_TOKEN" ]]; then
|
||||
echo "::error::Set PKGS_BOT_TOKEN; a GITHUB_TOKEN merge would not publish."
|
||||
exit 1
|
||||
fi
|
||||
# Idempotent: enabling twice errors. Bot lanes enable their own.
|
||||
if [[ "$(gh pr view "$PR" -R "$GITHUB_REPOSITORY" --json autoMergeRequest --jq '.autoMergeRequest != null')" == true ]]; then
|
||||
echo "auto-merge already enabled on #$PR"
|
||||
exit 0
|
||||
fi
|
||||
# --match-head-commit: never arm a merge for a commit newer than
|
||||
# the one just judged.
|
||||
gh pr merge --auto --squash --match-head-commit "$HEAD_SHA" "$PR" -R "$GITHUB_REPOSITORY"
|
||||
|
||||
# Removing build-approved withdraws the approval, so withdraw the
|
||||
# auto-merge it armed too. Only on that event: auto-merge a maintainer
|
||||
# enabled by hand on any other PR is theirs to keep.
|
||||
- name: Withdraw auto-merge
|
||||
if: >-
|
||||
steps.decide.outputs.enable == 'false' &&
|
||||
github.event.action == 'unlabeled' && github.event.label.name == 'build-approved'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||
PR: ${{ steps.pr.outputs.number }}
|
||||
run: |
|
||||
if [[ "$(gh pr view "$PR" -R "$GITHUB_REPOSITORY" --json autoMergeRequest --jq '.autoMergeRequest != null')" == true ]]; then
|
||||
gh pr merge --disable-auto "$PR" -R "$GITHUB_REPOSITORY"
|
||||
fi
|
||||
@@ -0,0 +1,319 @@
|
||||
name: Build changed packages
|
||||
|
||||
# Build every package directory a PR touches, one job per package per arch:
|
||||
# x86_64 on the self-hosted droplet pool, aarch64 natively on GitHub's arm64
|
||||
# runners. Artifacts are unsigned; publish.yml signs and publishes them on
|
||||
# merge.
|
||||
#
|
||||
# Tooling runs from the base branch; a PR supplies only pkgbuilds/. The
|
||||
# vouch gate limits who may spend compute; this limits what their PR can run.
|
||||
|
||||
# No paths filter: approved PRs must report the required `result` even when
|
||||
# no package directory changed. Those PRs get an empty matrix and a passing
|
||||
# result in seconds; unapproved PRs wait for maintainer approval.
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
packages:
|
||||
description: "Space-separated package directories to build"
|
||||
required: true
|
||||
|
||||
# A new push normally cancels the PR's in-flight build. The sync bots'
|
||||
# branches (auto/sync-*) are the exception: they are force-pushed with fresh
|
||||
# upstream releases several times a day, which kept cancelling multi-hour
|
||||
# aarch64 builds before they could finish. There the newest run waits
|
||||
# instead (GitHub keeps at most one pending run per group, replacing older
|
||||
# pending ones), and when it starts it reuses every artifact the finished
|
||||
# build uploaded, so only packages whose tree changed are built again.
|
||||
concurrency:
|
||||
group: build-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: ${{ !(github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'auto/sync-')) }}
|
||||
|
||||
jobs:
|
||||
# Builds cost real machines, so they run only for trusted authors:
|
||||
# collaborators, anyone in .github/VOUCHED.td (read from the default
|
||||
# branch, so a PR cannot vouch for itself), or a PR a maintainer has
|
||||
# labelled "build-approved". Everyone else gets this job's plan output
|
||||
# while the required `result` stays pending until a maintainer approves.
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.list.outputs.matrix }}
|
||||
count: ${{ steps.gate.outputs.count }}
|
||||
trusted: ${{ steps.gate.outputs.trusted }}
|
||||
vouch_status: ${{ steps.vouch.outputs.status }}
|
||||
empty: ${{ steps.list.outputs.empty }}
|
||||
steps:
|
||||
# Same rule as the build job: bin/build-matrix comes from the base
|
||||
# branch tip, the package directories from the PR head.
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref || github.sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.head.sha }}"
|
||||
git checkout "${{ github.event.pull_request.head.sha }}" -- pkgbuilds/
|
||||
# Bootstrap: the PR that introduces this tooling has a base without
|
||||
# it. Take the plan helper from the PR head in that one case; it
|
||||
# runs on a hosted runner and only prints a plan.
|
||||
if [[ ! -x bin/build-matrix ]]; then
|
||||
git checkout "${{ github.event.pull_request.head.sha }}" -- bin/build-matrix helpers/
|
||||
echo "::notice::base branch has no bin/build-matrix; using the PR's copy for planning"
|
||||
fi
|
||||
- id: vouch
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: mitchellh/vouch/action/check-user@f23dbb5e745334f97414ec70463ce7301071a661 # v1
|
||||
with:
|
||||
user: ${{ github.event.pull_request.user.login }}
|
||||
allow-fail: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- id: approval
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
...context.repo, pull_number: context.payload.pull_request.number,
|
||||
});
|
||||
// Approving or rerunning a held run keeps its original event,
|
||||
// which may predate the label. Read the current approval instead.
|
||||
core.setOutput('approved', pr.state === 'open' &&
|
||||
pr.head.sha === context.payload.pull_request.head.sha &&
|
||||
pr.labels.some(label => label.name === 'build-approved'));
|
||||
# One matrix entry per package per architecture. Every package builds
|
||||
# once, against edge; the channels it ships to on merge are carried
|
||||
# along for information. A filename means one set of bytes.
|
||||
- id: list
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [[ -n "${{ github.event.inputs.packages }}" ]]; then
|
||||
names="${{ github.event.inputs.packages }}"
|
||||
else
|
||||
# The PR's own files, as GitHub lists them against the merge base.
|
||||
# A two-dot diff against the current base tip also counted every
|
||||
# package master changed after the PR branched, so a stale PR
|
||||
# planned dozens of unrelated packages at its old versions. The
|
||||
# checkout here is shallow, so there is no merge base to diff from.
|
||||
# A package the PR deletes has nothing to build.
|
||||
names=$(gh api --paginate "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" --jq '.[].filename' \
|
||||
| awk -F/ '$1=="pkgbuilds" && NF>2 {print $2}' | sort -u \
|
||||
| while read -r name; do
|
||||
if git cat-file -e "${{ github.event.pull_request.head.sha }}:pkgbuilds/$name" 2>/dev/null; then echo "$name"; fi
|
||||
done)
|
||||
fi
|
||||
matrix=$(printf '%s\n' $names | bin/build-matrix)
|
||||
# A package directory whose exact tree already has a build artifact
|
||||
# (label <pkg>-<arch>-<tree hash>, uploaded only after a successful
|
||||
# build) is not built again. Pushing a fix for one package to a PR
|
||||
# that touches fifty rebuilds one, not fifty; publish.yml finds the
|
||||
# same artifacts on merge. workflow_dispatch is an explicit request
|
||||
# and always builds.
|
||||
if [[ "${{ github.event_name }}" == pull_request ]]; then
|
||||
head="${{ github.event.pull_request.head.sha }}"
|
||||
kept=(); reused=()
|
||||
while read -r entry; do
|
||||
package=$(jq -r .package <<<"$entry"); arch=$(jq -r .arch <<<"$entry")
|
||||
label="$package-$arch-$(git rev-parse "$head:pkgbuilds/$package")"
|
||||
found=$(curl -fsS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts?name=$label&per_page=5" \
|
||||
| jq -r '[.artifacts[] | select(.expired|not)] | length' || echo 0)
|
||||
if (( found > 0 )); then reused+=("$label"); else kept+=("$entry"); fi
|
||||
done < <(jq -c '.include[]' <<<"$matrix")
|
||||
matrix=$(printf '%s\n' "${kept[@]}" | jq -sc '{include: .}')
|
||||
if (( ${#reused[@]} )); then
|
||||
printf '==> already built, reusing the artifact: %s\n' "${reused[@]}"
|
||||
{ echo "Reused existing build artifacts (${#reused[@]}):"; printf -- '- %s\n' "${reused[@]}"; } >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
fi
|
||||
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||
echo "planned=$(jq '.include | length' <<<"$matrix")" >> "$GITHUB_OUTPUT"
|
||||
jq -r '.include[] | "\(.package) \(.arch) -> \(.channels)"' <<<"$matrix"
|
||||
# A PR whose diff against its base is empty changes nothing: its
|
||||
# content already landed some other way (a sync PR beat it, or a
|
||||
# merge from master swallowed it). Merging it would record a change
|
||||
# that isn't one. Flag it so `result` fails rather than passes.
|
||||
if [[ "${{ github.event_name }}" == pull_request ]]; then
|
||||
total=$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}" | wc -l)
|
||||
echo "empty=$([[ $total -eq 0 ]] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
||||
echo "files changed vs base: $total"
|
||||
else
|
||||
echo "empty=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- id: gate
|
||||
env:
|
||||
STATUS: ${{ github.event_name == 'workflow_dispatch' && 'dispatch' || steps.vouch.outputs.status }}
|
||||
AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
APPROVED: ${{ steps.approval.outputs.approved || 'false' }}
|
||||
PLANNED: ${{ steps.list.outputs.planned }}
|
||||
run: |
|
||||
case "$STATUS" in
|
||||
bot|collaborator|vouched|dispatch) trusted=true ;;
|
||||
# A denouncement is absolute: the label cannot override it.
|
||||
denounced) trusted=false ;;
|
||||
unknown) trusted=$APPROVED ;;
|
||||
*) trusted=false ;;
|
||||
esac
|
||||
echo "trusted=$trusted" >> "$GITHUB_OUTPUT"
|
||||
if [[ $trusted == true ]]; then
|
||||
echo "count=$PLANNED" >> "$GITHUB_OUTPUT"
|
||||
echo "Author $AUTHOR is trusted ($STATUS); building $PLANNED package(s)."
|
||||
else
|
||||
echo "count=0" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::Author $AUTHOR is not trusted ($STATUS). $PLANNED package build(s) planned but not run."
|
||||
if [[ $STATUS == denounced ]]; then
|
||||
echo "::warning::The author is denounced in .github/VOUCHED.td; the build-approved label does not apply."
|
||||
else
|
||||
echo "::warning::A maintainer can add the author to .github/VOUCHED.td, or apply the 'build-approved' label to build this PR."
|
||||
fi
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.count != '0'
|
||||
# The droplets are x86, so aarch64 there runs under QEMU: omarchy-mac-boot
|
||||
# took 2h47m of the 180 minutes, most of it in check().
|
||||
# GitHub's arm64 runners (4 vCPU, 16 GB; ~100 GB disk free in our pilots)
|
||||
# build it natively in 11 minutes, and linux-aurora in 30 (72 under QEMU).
|
||||
runs-on: ${{ matrix.arch == 'aarch64' && 'ubuntu-24.04-arm' || fromJSON('["self-hosted","omarchy-builder"]') }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.changes.outputs.matrix) }}
|
||||
steps:
|
||||
# Tooling from base: everything that executes on this runner's host
|
||||
# (bin/, helpers/, build/) comes from the base branch. Only the PR's
|
||||
# package directories are overlaid. A PR can therefore change what
|
||||
# gets built, never how the runner builds it. A PR that changes both
|
||||
# tooling and a package builds the package with the OLD tooling; land
|
||||
# the tooling first. workflow_dispatch has no PR and runs as checked out.
|
||||
# The base branch tip, not the event's base.sha: that sha is a snapshot
|
||||
# taken at the PR's last push, so a tooling fix on master would never
|
||||
# reach an open PR until someone pushed to it (seen on the daily sync
|
||||
# PR after the artifact packing fix landed).
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref || github.sha }}
|
||||
persist-credentials: false
|
||||
- name: Overlay the PR's package directories onto base tooling
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.head.sha }}"
|
||||
git checkout "${{ github.event.pull_request.head.sha }}" -- pkgbuilds/
|
||||
echo "tooling: $(git rev-parse --short HEAD) (base) packages: ${{ github.event.pull_request.head.sha }} (PR head)"
|
||||
# A preview only. `head` exits after ten lines and, under pipefail,
|
||||
# git's SIGPIPE (141) failed the step for any PR far enough behind
|
||||
# master to differ in more files; sed reads the whole stream.
|
||||
git status --short | sed -n '1,10p'
|
||||
# A PR can change a package's directory without changing its version:
|
||||
# an upstream hook, its tests, a README. edge already holds that
|
||||
# version, so the planner builds nothing and there is nothing to pack.
|
||||
# The same check publish.yml's rebuild job makes; without it the pack
|
||||
# step failed on the empty output and the required result went red.
|
||||
- name: Build ${{ matrix.package }} (${{ matrix.arch }}, ships to ${{ matrix.channels }})
|
||||
id: build
|
||||
env:
|
||||
CONTAINER_ENGINE: docker
|
||||
run: |
|
||||
set -euo pipefail
|
||||
plan=$(bin/build --dry-run --mirror edge --arch "${{ matrix.arch }}" --package "${{ matrix.package }}" 2>&1 | grep -E '^==> Plan complete' || true)
|
||||
if [[ -n "$plan" && -z "$(sed -E 's/.*would build: *//' <<<"$plan" | tr -d '[:space:]')" ]]; then
|
||||
echo "==> ${{ matrix.package }} (${{ matrix.arch }}): edge already holds this version, nothing to build"
|
||||
echo "built=false" >> "$GITHUB_OUTPUT"
|
||||
# Arch bumps pkgrel only when the built package changes, which is
|
||||
# the reviewer's call. When the recipe itself changed (comments and
|
||||
# blank lines aside), flag it on the PKGBUILD without failing: the
|
||||
# change ships only once pkgver or pkgrel moves. HEAD is the base
|
||||
# branch; the PR's package directories are overlaid on it.
|
||||
recipe="pkgbuilds/${{ matrix.package }}/PKGBUILD"
|
||||
recipe_lines() { grep -vE '^[[:space:]]*(#|$)' || true; }
|
||||
if ! cmp -s <(git show "HEAD:$recipe" 2>/dev/null | recipe_lines) <(recipe_lines < "$recipe"); then
|
||||
note="PKGBUILD changed, but edge already holds this version of ${{ matrix.package }}, so the change will not ship until pkgver or pkgrel changes. Bump pkgrel if it affects the built package."
|
||||
echo "::warning file=$recipe,title=Change will not ship::$note"
|
||||
echo "$note" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
bin/build --mirror edge --arch "${{ matrix.arch }}" --package "${{ matrix.package }}"
|
||||
echo "built=true" >> "$GITHUB_OUTPUT"
|
||||
# makepkg's check() leaves meson's per-test output in the build tree,
|
||||
# never on stdout, so a failing test shows only a summary line in this
|
||||
# job log. bin/build bind-mounts $SRC_DIR at /src, so those logs outlive
|
||||
# the container. Without this upload an arch-specific test failure
|
||||
# cannot be diagnosed from CI at all (seen on owe 0.2.7, aarch64).
|
||||
- name: Upload test logs
|
||||
if: always() && steps.build.outcome == 'failure'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-logs-${{ matrix.package }}-${{ matrix.arch }}
|
||||
path: src/**/meson-logs/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
# The artifact label carries the package directory's git tree hash so
|
||||
# the publish step can find the build for exactly the tree that merged.
|
||||
# The package file inside keeps makepkg's standard name untouched.
|
||||
# The artifact label uses the PR head's tree for this package: that is
|
||||
# the tree that merges, and what publish looks up.
|
||||
- name: Tree hash
|
||||
id: tree
|
||||
run: echo "hash=$(git rev-parse "${{ github.event.pull_request.head.sha || github.sha }}:pkgbuilds/${{ matrix.package }}")" >> "$GITHUB_OUTPUT"
|
||||
# The upload action rejects a path containing ':', which is how makepkg
|
||||
# names a package with an epoch. The files ride inside packages.tar
|
||||
# (helpers/artifact-helpers.sh); publish.yml unpacks it. Only a
|
||||
# successful build uploads: the artifact's existence is what lets the
|
||||
# planner above and publish.yml skip rebuilding this exact tree.
|
||||
- name: Pack artifact
|
||||
if: steps.build.outputs.built == 'true'
|
||||
id: pack
|
||||
run: |
|
||||
source helpers/artifact-helpers.sh
|
||||
pack_packages build-output/edge/${{ matrix.arch }} packages.tar
|
||||
tar -tvf packages.tar
|
||||
- name: Upload artifact
|
||||
if: steps.build.outputs.built == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.package }}-${{ matrix.arch }}-${{ steps.tree.outputs.hash }}
|
||||
path: packages.tar
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
# `result` is required by branch protection. An unvouched author awaiting
|
||||
# approval gets a differently named informational check, leaving `result`
|
||||
# unreported (pending). Skipping or passing a job named `result` would count
|
||||
# as satisfying the requirement even though no build was authorized.
|
||||
# Actual planning/build failures and denouncements still report `result`.
|
||||
result:
|
||||
name: ${{ needs.changes.result == 'success' && needs.changes.outputs.trusted == 'false' && needs.changes.outputs.vouch_status == 'unknown' && needs.changes.outputs.empty == 'false' && 'Awaiting build approval' || 'result' }}
|
||||
needs: [changes, build]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "trusted=${{ needs.changes.outputs.trusted }} build=${{ needs.build.result }}"
|
||||
if [[ "${{ needs.changes.result }}" != "success" ]]; then
|
||||
echo "::error::Build planning or the trust check failed. See the changes job."
|
||||
exit 1
|
||||
fi
|
||||
# Nothing to merge: the PR's diff against its base is empty. Its
|
||||
# change already landed elsewhere. Close it rather than merge it.
|
||||
if [[ "${{ needs.changes.outputs.empty }}" == "true" ]]; then
|
||||
echo "::error::This PR changes no files relative to its base. Its content is already on the target branch; close it instead of merging."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${{ needs.changes.outputs.trusted }}" == "false" && "${{ needs.changes.outputs.vouch_status }}" == "unknown" && "${{ needs.changes.outputs.empty }}" == "false" ]]; then
|
||||
echo "::notice::Awaiting maintainer build approval. Apply 'build-approved' to this PR or vouch for the author in .github/VOUCHED.td."
|
||||
echo "Package builds are waiting for maintainer approval. Apply **build-approved** to this PR to start them. The required **result** check remains pending." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${{ needs.changes.outputs.trusted }}" != "true" ]]; then
|
||||
echo "::error::Builds are blocked: the author is denounced or the trust result is invalid. The build-approved label cannot override this."
|
||||
exit 1
|
||||
fi
|
||||
[[ "${{ needs.build.result }}" == "success" || "${{ needs.build.result }}" == "skipped" ]]
|
||||
@@ -0,0 +1,118 @@
|
||||
name: Refresh builder images
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '23 4 * * *'
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- build/**
|
||||
- bin/builder-image
|
||||
- helpers/paths.sh
|
||||
- helpers/docker-helpers.sh
|
||||
- tests/build-isolation.sh
|
||||
- .github/workflows/builder-images.yml
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- build/**
|
||||
- bin/builder-image
|
||||
- helpers/paths.sh
|
||||
- helpers/docker-helpers.sh
|
||||
- tests/build-isolation.sh
|
||||
- .github/workflows/builder-images.yml
|
||||
|
||||
# Complete each refresh before another can replace its tested image tags.
|
||||
concurrency:
|
||||
group: builder-images-${{ github.event.pull_request.number || 'master' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# Exercise proposed image changes on native runners with a read-only token.
|
||||
# Publishing is a separate master-only job with its own write permission.
|
||||
validate:
|
||||
if: github.event_name == 'pull_request'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CONTAINER_ENGINE: docker
|
||||
CANDIDATE_IMAGE: omarchy-builder-candidate:${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Build a fresh environment
|
||||
run: bin/builder-image build --arch "${{ matrix.arch }}" --mirror edge --tag "$CANDIDATE_IMAGE" --fresh
|
||||
- name: Test isolated package builds
|
||||
env:
|
||||
TEST_BUILDER_IMAGE: ${{ env.CANDIDATE_IMAGE }}
|
||||
run: tests/build-isolation.sh
|
||||
|
||||
refresh:
|
||||
if: github.repository == 'omacom/omarchy-pkgs' && github.ref == 'refs/heads/master'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
env:
|
||||
CONTAINER_ENGINE: docker
|
||||
REGISTRY_IMAGE: ghcr.io/omacom/omarchy-pkg-builder
|
||||
CANDIDATE_IMAGE: omarchy-builder-candidate:${{ matrix.arch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Build a fresh environment
|
||||
run: bin/builder-image build --arch "${{ matrix.arch }}" --mirror edge --tag "$CANDIDATE_IMAGE" --fresh
|
||||
- name: Test isolated package builds
|
||||
env:
|
||||
TEST_BUILDER_IMAGE: ${{ env.CANDIDATE_IMAGE }}
|
||||
run: tests/build-isolation.sh
|
||||
- name: Publish tested image
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_ACTOR: ${{ github.actor }}
|
||||
DOCKER_CONFIG: ${{ runner.temp }}/builder-registry-auth
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$DOCKER_CONFIG"
|
||||
trap 'rm -rf "$DOCKER_CONFIG"' EXIT
|
||||
printf '%s' "$GH_TOKEN" | docker login ghcr.io --username "$GH_ACTOR" --password-stdin
|
||||
key=$(bin/builder-image key --arch "${{ matrix.arch }}" --mirror edge)
|
||||
version="$REGISTRY_IMAGE:$key-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
|
||||
docker tag "$CANDIDATE_IMAGE" "$version"
|
||||
docker push "$version"
|
||||
# GHCR creates new packages private. Do not advertise an image to
|
||||
# fork PRs until it is public. This is a one-time package setting.
|
||||
anonymous_config=$(mktemp -d "$RUNNER_TEMP/builder-anonymous.XXXXXX")
|
||||
if ! DOCKER_CONFIG="$anonymous_config" docker manifest inspect "$version" >/dev/null; then
|
||||
rm -rf "$anonymous_config"
|
||||
echo "::error::Make the omacom/omarchy-pkg-builder GHCR package public, then rerun this job. The previous matching image remains selected."
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$anonymous_config"
|
||||
docker tag "$CANDIDATE_IMAGE" "$REGISTRY_IMAGE:$key"
|
||||
docker push "$REGISTRY_IMAGE:$key"
|
||||
digest=$(docker image inspect "$version" --format '{{index .RepoDigests 0}}')
|
||||
printf '### Builder image (%s)\n\nInput key: `%s`\n\nImage: `%s`\n' \
|
||||
"${{ matrix.arch }}" "$key" "$digest" >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -0,0 +1,455 @@
|
||||
name: Publish merged packages
|
||||
|
||||
# On every push to master: for each package directory the push touched and
|
||||
# each architecture it supports, find the PR build artifact for exactly that
|
||||
# tree (label = <pkg>-<arch>-<treehash>), or build it now when there is
|
||||
# none, then publish that one artifact into every channel the package ships
|
||||
# to. One build, one file, several databases: a filename means one set of
|
||||
# bytes everywhere, and channels are views over a shared pool.
|
||||
#
|
||||
# Secrets live in the "publish" environment, restricted to master:
|
||||
# GPG_PRIVATE_KEY, GPG_PASSPHRASE the channel signing key
|
||||
# R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT
|
||||
# OMARCHY_PUBLISH_PREFIX (environment variable, not secret) points a proof
|
||||
# run at a scratch prefix inside the live bucket; empty means the real
|
||||
# channel paths.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths: ["pkgbuilds/**"]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
packages:
|
||||
description: "Space-separated package directories to publish from master"
|
||||
required: true
|
||||
|
||||
# Only the publish job serializes (see its concurrency group): two publishes
|
||||
# into one channel at once would race on the database. Builds run outside the
|
||||
# lock, so a merge waits behind another merge's signing and upload, seconds,
|
||||
# never behind its kernel build.
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.list.outputs.matrix }}
|
||||
count: ${{ steps.list.outputs.count }}
|
||||
rebuild: ${{ steps.list.outputs.rebuild }}
|
||||
rebuild_count: ${{ steps.list.outputs.rebuild_count }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- id: list
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "${{ github.event.inputs.packages }}" ]]; then
|
||||
names="${{ github.event.inputs.packages }}"
|
||||
else
|
||||
names=$(git diff --name-only "${{ github.event.before }}..${{ github.sha }}" -- pkgbuilds \
|
||||
| awk -F/ '$1=="pkgbuilds" && NF>2 {print $2}' | sort -u)
|
||||
fi
|
||||
matrix=$(printf '%s\n' $names | bin/build-matrix)
|
||||
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||
echo "count=$(jq '.include | length' <<<"$matrix")" >> "$GITHUB_OUTPUT"
|
||||
jq -r '.include[] | "\(.package) \(.arch) -> \(.channels)"' <<<"$matrix"
|
||||
# Reuse or rebuild, decided per entry and said out loud. A tree with
|
||||
# no build artifact (PR artifacts last 7 days; a dispatch may name
|
||||
# any package) goes to the rebuild job: aarch64 natively on GitHub's
|
||||
# arm64 runner, x86_64 on a builder droplet, one runner per entry.
|
||||
rebuild=()
|
||||
echo "| package | arch | tree | decision |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| --- | --- | --- | --- |" >> "$GITHUB_STEP_SUMMARY"
|
||||
while read -r entry; do
|
||||
package=$(jq -r .package <<<"$entry"); arch=$(jq -r .arch <<<"$entry")
|
||||
hash=$(git rev-parse "HEAD:pkgbuilds/$package")
|
||||
label="$package-$arch-$hash"
|
||||
found=$(curl -fsS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts?name=$label&per_page=5" \
|
||||
| jq -r '[.artifacts[] | select(.expired|not)] | sort_by(.created_at) | last | select(. != null) | "run \(.workflow_run.id), expires \(.expires_at)"')
|
||||
# The plan the publish job makes, made here before a runner is
|
||||
# asked for: a tree the channel already holds at master's version
|
||||
# (a re-run, or a dispatch naming a package that is fine) needs
|
||||
# no build, and an x86 rebuild would wait minutes for a droplet
|
||||
# to find that out in seconds.
|
||||
plan=""
|
||||
[[ -n "$found" ]] || plan=$(bin/build --dry-run --mirror edge --arch "$arch" --package "$package" 2>&1 | grep -E '^==> Plan complete' || true)
|
||||
if [[ -n "$found" ]]; then
|
||||
decision="reuse the build artifact ($found)"
|
||||
elif [[ -n "$plan" && -z "$(sed -E 's/.*would build: *//' <<<"$plan" | tr -d '[:space:]')" ]]; then
|
||||
decision="already published at master's version, nothing to build"
|
||||
elif [[ $arch == aarch64 ]]; then
|
||||
decision="no build artifact: rebuild natively on ubuntu-24.04-arm"
|
||||
rebuild+=("$(jq -c '. + {runner: "[\"ubuntu-24.04-arm\"]"}' <<<"$entry")")
|
||||
else
|
||||
decision="no build artifact: rebuild on a builder droplet"
|
||||
rebuild+=("$(jq -c '. + {runner: "[\"self-hosted\",\"omarchy-builder\"]"}' <<<"$entry")")
|
||||
fi
|
||||
echo "==> $label: $decision"
|
||||
echo "| $package | $arch | ${hash:0:12} | $decision |" >> "$GITHUB_STEP_SUMMARY"
|
||||
done < <(jq -c '.include[]' <<<"$matrix")
|
||||
echo "rebuild=$(printf '%s\n' "${rebuild[@]}" | jq -sc '{include: .}')" >> "$GITHUB_OUTPUT"
|
||||
echo "rebuild_count=${#rebuild[@]}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# "Build it now when there is none". Each entry builds exactly as
|
||||
# build-pr.yml builds it (same runner kind, same builder image, same
|
||||
# bin/build call) and uploads under the same label, so the publish job
|
||||
# collects this run's artifact the way it collects a PR's. No secret
|
||||
# reaches these runners, and they hold no lock: entries build in parallel,
|
||||
# and other merges publish while they do.
|
||||
rebuild:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.rebuild_count != '0'
|
||||
runs-on: ${{ fromJSON(matrix.runner) }}
|
||||
# The droplets are x86, so aarch64 never builds there. omarchy-mac-boot
|
||||
# under QEMU took 2h47m; native arm64 and x86 kernels fit easily.
|
||||
timeout-minutes: 240
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.changes.outputs.rebuild) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
# The same check the publish job makes before building: a re-run for a
|
||||
# package the channel already holds at master's version builds
|
||||
# nothing, and uploads nothing that could shadow the published file.
|
||||
- name: Build ${{ matrix.package }} (${{ matrix.arch }})
|
||||
id: build
|
||||
env:
|
||||
CONTAINER_ENGINE: docker
|
||||
run: |
|
||||
set -euo pipefail
|
||||
plan=$(bin/build --dry-run --mirror edge --arch "${{ matrix.arch }}" --package "${{ matrix.package }}" 2>&1 | grep -E '^==> Plan complete' || true)
|
||||
if [[ -n "$plan" && -z "$(sed -E 's/.*would build: *//' <<<"$plan" | tr -d '[:space:]')" ]]; then
|
||||
echo "==> ${{ matrix.package }} (${{ matrix.arch }}): already published at master's version, nothing to build"
|
||||
echo "built=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
bin/build --mirror edge --arch "${{ matrix.arch }}" --package "${{ matrix.package }}"
|
||||
echo "built=true" >> "$GITHUB_OUTPUT"
|
||||
- name: Pack artifact
|
||||
if: steps.build.outputs.built == 'true'
|
||||
id: pack
|
||||
run: |
|
||||
source helpers/artifact-helpers.sh
|
||||
pack_packages build-output/edge/${{ matrix.arch }} packages.tar
|
||||
tar -tvf packages.tar
|
||||
echo "label=${{ matrix.package }}-${{ matrix.arch }}-$(git rev-parse "HEAD:pkgbuilds/${{ matrix.package }}")" >> "$GITHUB_OUTPUT"
|
||||
- name: Upload artifact
|
||||
if: steps.build.outputs.built == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.pack.outputs.label }}
|
||||
path: packages.tar
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
# One job for the whole merge. It collects every artifact for the merged
|
||||
# tree (PR builds, or the rebuild job above), then walks each channel and
|
||||
# architecture slot exactly once: pull that database, add every package
|
||||
# that belongs in it, upload. Six slots, six round trips, however many
|
||||
# packages the merge carried. One process is the only writer, so there
|
||||
# is no race between packages; the concurrency group keeps one merge's
|
||||
# publish from overlapping the next. It builds nothing, so it runs on a
|
||||
# hosted runner in about a minute instead of waiting for a droplet.
|
||||
# It waits for the rebuild job and runs whatever that job's result: a
|
||||
# failed rebuild leaves its package without an artifact, and the collect
|
||||
# step below records that and stops before any publish.
|
||||
publish:
|
||||
needs: [changes, rebuild]
|
||||
if: ${{ !cancelled() && needs.changes.result == 'success' && needs.changes.outputs.count != '0' }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: publish
|
||||
timeout-minutes: 30
|
||||
concurrency:
|
||||
group: publish
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Every matrix entry, as a file the shell steps can loop over:
|
||||
# package arch channels publish_arches
|
||||
- name: Plan
|
||||
run: |
|
||||
jq -r '.include[] | "\(.package) \(.arch) \(.channels|gsub(" ";",")) \(.publish_arches|gsub(" ";","))"' \
|
||||
<<'EOF_MATRIX' > plan.txt
|
||||
${{ needs.changes.outputs.matrix }}
|
||||
EOF_MATRIX
|
||||
cat plan.txt
|
||||
|
||||
# Fetch each package's artifact into build-output/edge/<arch>/: the PR's,
|
||||
# or the rebuild job's when the PR's had expired. An artifact
|
||||
# carries its package files inside packages.tar (see build-pr.yml and
|
||||
# helpers/artifact-helpers.sh: the upload action rejects the colon in
|
||||
# an epoch filename).
|
||||
- name: Collect artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CONTAINER_ENGINE: docker
|
||||
run: |
|
||||
set -uo pipefail
|
||||
source helpers/artifact-helpers.sh
|
||||
# sources.jsonl: where each package's files came from, or that the
|
||||
# build failed. A failed build ends the run before any publish, and
|
||||
# the record says so instead of the report job finding nothing.
|
||||
: > sources.jsonl
|
||||
failed=0
|
||||
while read -r package arch channels publish_arches; do
|
||||
hash=$(git rev-parse "HEAD:pkgbuilds/$package")
|
||||
label="$package-$arch-$hash"
|
||||
found=$(curl -fsS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts?name=$label&per_page=5" \
|
||||
| jq -r '[.artifacts[] | select(.expired|not)] | sort_by(.created_at) | last | select(. != null) | "\(.archive_download_url) \(.workflow_run.id)"')
|
||||
read -r found from_run <<<"$found" || true
|
||||
mkdir -p "build-output/edge/$arch"
|
||||
if [[ -n "$found" ]]; then
|
||||
if [[ $from_run == "${{ github.run_id }}" ]]; then
|
||||
kind=rebuild
|
||||
echo "==> $label: artifact from this run's $arch rebuild"
|
||||
else
|
||||
kind=pr-artifact
|
||||
echo "==> $label: reusing the build artifact from run $from_run"
|
||||
fi
|
||||
rm -rf /tmp/artifact; mkdir -p /tmp/artifact
|
||||
if curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -o /tmp/artifact.zip "$found" \
|
||||
&& unzip -oq /tmp/artifact.zip -d /tmp/artifact \
|
||||
&& unpack_packages /tmp/artifact "build-output/edge/$arch"; then
|
||||
jq -nc --arg p "$package" --arg a "$arch" --arg s "$kind" '{package:$p, arch:$a, source:$s}' >> sources.jsonl
|
||||
else
|
||||
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"artifact-download-failed"}' >> sources.jsonl; failed=1; break
|
||||
fi
|
||||
else
|
||||
# bin/build plans against the public channel first. If the
|
||||
# channel already holds master's version there is nothing to
|
||||
# build and nothing to publish: a re-run for a package that
|
||||
# turned out to be fine. Record it and move on.
|
||||
plan=$(CONTAINER_ENGINE=docker bin/build --dry-run --mirror edge --arch "$arch" --package "$package" 2>&1 | grep -E '^==> Plan complete' || true)
|
||||
# "Packages that would build:" followed by nothing means none.
|
||||
if [[ -n "$plan" && -z "$(sed -E 's/.*would build: *//' <<<"$plan" | tr -d '[:space:]')" ]]; then
|
||||
echo "==> $label: already published at master's version, nothing to do"
|
||||
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"already-published"}' >> sources.jsonl
|
||||
continue
|
||||
fi
|
||||
# Nothing builds here. No artifact means the rebuild failed (see
|
||||
# the rebuild job), or an artifact expired between planning and
|
||||
# now (re-run all jobs).
|
||||
echo "::error::$label: no artifact from the rebuild job"
|
||||
jq -nc --arg p "$package" --arg a "$arch" '{package:$p, arch:$a, source:"build-failed"}' >> sources.jsonl; failed=1; break
|
||||
fi
|
||||
done < plan.txt
|
||||
ls -1 build-output/edge/*/*.pkg.tar.zst 2>/dev/null || true
|
||||
if (( failed )); then
|
||||
# Write the record now; the publish step will not run.
|
||||
jq -n --arg sha "${{ github.sha }}" --arg run "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
||||
--arg prefix "${OMARCHY_PUBLISH_PREFIX:-}" --arg event "${{ github.event_name }}" \
|
||||
--slurpfile sources sources.jsonl --slurpfile plan <(jq -c '.include[]' <<<'${{ needs.changes.outputs.matrix }}' | jq -sc .) \
|
||||
'{time: (now|todate), commit:$sha, run:$run, event:$event, target: (if $prefix=="" then "live" else $prefix end), plan:$plan[0], sources:$sources, slots:[]}' \
|
||||
> publish-record.json
|
||||
cat publish-record.json
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
RCLONE_CONFIG_R2_TYPE: s3
|
||||
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
|
||||
# The token is scoped to the bucket; it may not CreateBucket, and
|
||||
# rclone's existence check is a CreateBucket in disguise.
|
||||
RCLONE_CONFIG_R2_NO_CHECK_BUCKET: "true"
|
||||
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
OMARCHY_PUBLISH_PREFIX: ${{ vars.OMARCHY_PUBLISH_PREFIX }}
|
||||
# repo-add, gpg and bsdtar are Arch tools; run the publish inside the
|
||||
# builder image (host-native, edge) with the workspace mounted.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! compgen -G "build-output/edge/*/*.pkg.tar.zst" >/dev/null; then
|
||||
echo "Nothing to publish: every requested package is already published at master's version."
|
||||
jq -n --arg sha "${{ github.sha }}" --arg run "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
||||
--arg prefix "${OMARCHY_PUBLISH_PREFIX:-}" --arg event "${{ github.event_name }}" \
|
||||
--slurpfile sources sources.jsonl --slurpfile plan <(jq -c '.include[]' <<<'${{ needs.changes.outputs.matrix }}' | jq -sc .) \
|
||||
'{time: (now|todate), commit:$sha, run:$run, event:$event, target: (if $prefix=="" then "live" else $prefix end), plan:$plan[0], sources:$sources, slots:[]}' \
|
||||
> publish-record.json
|
||||
cat publish-record.json
|
||||
exit 0
|
||||
fi
|
||||
# A fresh runner has no images, and building this one here cost
|
||||
# every publish about 100 s (and 20 s more to start a container
|
||||
# from it) for the 14 s of signing and upload it is needed for.
|
||||
# builder-images.yml already publishes the tested image for exactly
|
||||
# these build inputs under their key; pull that. Build only when no
|
||||
# image carries the key: a merge that changed build/ publishes
|
||||
# before the refresh it triggered has finished.
|
||||
builder=omarchy-pkg-builder:latest-x86_64-edge
|
||||
if ! docker image inspect "$builder" >/dev/null 2>&1; then
|
||||
key=$(bin/builder-image key --arch x86_64 --mirror edge)
|
||||
published="ghcr.io/omacom/omarchy-pkg-builder:$key"
|
||||
if docker pull --quiet "$published" &&
|
||||
[[ $(docker image inspect "$published" --format '{{index .Config.Labels "org.omarchy.builder.key"}}') == "$key" ]]; then
|
||||
docker tag "$published" "$builder"
|
||||
echo "==> Builder image: pulled $published"
|
||||
else
|
||||
echo "==> Builder image: none published for $key, building it"
|
||||
docker buildx build --load -t "$builder" --build-arg MIRROR=edge build
|
||||
fi
|
||||
fi
|
||||
|
||||
# Group the merge's files by the (channel, architecture) slot each
|
||||
# belongs to. A package's files live under build-output/edge/<built
|
||||
# arch>/ and are named <pkgname>-<ver>-<arch|any>.pkg.tar.zst; a
|
||||
# split package's outputs share the pkgbase's directory, so match
|
||||
# on the artifact list rather than the name.
|
||||
# pkgbase is read inside the builder image: the Ubuntu host has no
|
||||
# bsdtar. One container call maps every file to its pkgbase.
|
||||
docker run --rm -v "$PWD:/w:ro" -w /w omarchy-pkg-builder:latest-x86_64-edge bash -c '
|
||||
for f in build-output/edge/*/*.pkg.tar.zst; do
|
||||
printf "%s %s\n" "$f" "$(bsdtar -xOf "$f" .PKGINFO | awk -F" = " "\$1==\"pkgbase\"{print \$2}")"
|
||||
done' > pkgbase.txt
|
||||
declare -A slot_files=()
|
||||
while read -r package arch channels publish_arches; do
|
||||
for f in build-output/edge/"$arch"/*.pkg.tar.zst; do
|
||||
# Only files this package produced (its PKGINFO pkgbase).
|
||||
[[ $(awk -v f="$f" '$1==f{print $2}' pkgbase.txt) == "$package" ]] || continue
|
||||
for mirror in ${channels//,/ }; do
|
||||
for parch in ${publish_arches//,/ }; do
|
||||
slot_files["$mirror/$parch"]+="$f "
|
||||
done
|
||||
done
|
||||
done
|
||||
done < plan.txt
|
||||
|
||||
# Deterministic slot order: edge before rc before stable, x86_64
|
||||
# before aarch64, so a failure leaves the earlier rings consistent.
|
||||
# Every slot's outcome goes into publish-record.json for the report
|
||||
# job: what was published, where, from which artifact, and whether
|
||||
# the slot succeeded. A failing slot stops the loop (set -e) but the
|
||||
# record still shows everything before it landed.
|
||||
: > slots.jsonl
|
||||
record_slot() { jq -nc --arg m "$1" --arg a "$2" --arg st "$3" --arg files "$4" \
|
||||
'{mirror:$m, arch:$a, status:$st, packages:[$files | split(" ") | .[] | select(length>0) | sub(".*/";"") | sub("\\.pkg\\.tar\\.zst$";"")]}' >> slots.jsonl; }
|
||||
status=0
|
||||
for mirror in edge rc stable; do
|
||||
for parch in x86_64 aarch64; do
|
||||
files=${slot_files["$mirror/$parch"]:-}
|
||||
[[ -n "$files" ]] || continue
|
||||
echo "==> $mirror/$parch: $files"
|
||||
if docker run --rm \
|
||||
-e GPG_PRIVATE_KEY -e GPG_PASSPHRASE -e OMARCHY_PUBLISH_PREFIX \
|
||||
-e RCLONE_CONFIG_R2_TYPE -e RCLONE_CONFIG_R2_PROVIDER -e RCLONE_CONFIG_R2_ENDPOINT -e RCLONE_CONFIG_R2_NO_CHECK_BUCKET \
|
||||
-e RCLONE_CONFIG_R2_ACCESS_KEY_ID -e RCLONE_CONFIG_R2_SECRET_ACCESS_KEY \
|
||||
-v "$PWD:/w:ro" -w /w \
|
||||
omarchy-pkg-builder:latest-x86_64-edge \
|
||||
bin/publish-artifact --remote R2:omarchy-pkgs --mirror "$mirror" --arch "$parch" $files; then
|
||||
record_slot "$mirror" "$parch" published "$files"
|
||||
else
|
||||
record_slot "$mirror" "$parch" failed "$files"
|
||||
status=1
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
done
|
||||
jq -n --arg sha "${{ github.sha }}" --arg run "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
||||
--arg prefix "${OMARCHY_PUBLISH_PREFIX:-}" --arg event "${{ github.event_name }}" \
|
||||
--slurpfile slots slots.jsonl --slurpfile sources sources.jsonl --slurpfile plan <(jq -c '.include[]' <<<'${{ needs.changes.outputs.matrix }}' | jq -sc .) \
|
||||
'{time: (now|todate), commit:$sha, run:$run, event:$event, target: (if $prefix=="" then "live" else $prefix end), plan:$plan[0], sources:$sources, slots:$slots}' \
|
||||
> publish-record.json
|
||||
cat publish-record.json
|
||||
exit $status
|
||||
|
||||
- name: Keep the publish record
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: publish-record-${{ github.run_id }}
|
||||
path: publish-record.json
|
||||
retention-days: 90
|
||||
|
||||
# Tell people what happened. A comment on the merged PR (found by the
|
||||
# merge commit, so squash and rebase merges work too) and a line appended
|
||||
# to a running JSON log in the bucket, next to the packages it describes,
|
||||
# so the history is public and can be rendered later.
|
||||
report:
|
||||
needs: [changes, publish]
|
||||
if: always() && needs.publish.result != 'skipped'
|
||||
runs-on: ubuntu-latest
|
||||
environment: publish
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: publish-record-${{ github.run_id }}
|
||||
- name: Render
|
||||
id: render
|
||||
run: |
|
||||
jq -r --arg outcome "${{ needs.publish.result }}" '
|
||||
def pkgs: [.packages[] | select(test("-debug-")|not)] | join(", ");
|
||||
def src: if .source=="pr-artifact" then "PR artifact" elif .source=="rebuild" or .source=="native-rebuild" then "rebuilt at merge" elif .source=="built" then "built here" elif .source=="already-published" then "already published, skipped" else "**" + .source + "**" end;
|
||||
"### Publish " + (if $outcome=="success" then "succeeded" else "FAILED" end) +
|
||||
" → **" + .target + "**" + (if .target!="live" then " (proof prefix, not live)" else "" end),
|
||||
"",
|
||||
"Packages: " + ([.sources[] | .package + " (" + .arch + ", " + src + ")"] | join("; ")),
|
||||
"",
|
||||
(if (.slots|length) > 0 then (.slots[] | "- `" + .mirror + "/" + .arch + "`: " + (if .status=="published" then "" else "**" + .status + "** " end) + pkgs)
|
||||
elif ([.sources[] | select(.source=="already-published")] | length) == (.sources|length) then "_Nothing to publish: already at master'"'"'s version everywhere._"
|
||||
else "_Nothing was published._" end),
|
||||
"",
|
||||
(if ([.sources[] | select(.source|test("failed"))] | length) > 0 then "_A build failed, so no channel was touched._\n"
|
||||
elif (.plan|length) > (.slots|length) then "_Some planned slots did not run because an earlier slot failed._\n" else "" end),
|
||||
"Commit " + .commit[0:7] + " · [run](" + .run + ")"
|
||||
' publish-record.json > comment.md
|
||||
cat comment.md
|
||||
- name: Append to the publish log in the bucket
|
||||
env:
|
||||
RCLONE_CONFIG_R2_TYPE: s3
|
||||
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
|
||||
RCLONE_CONFIG_R2_NO_CHECK_BUCKET: "true"
|
||||
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
run: |
|
||||
curl -fsSL https://downloads.rclone.org/rclone-current-linux-amd64.zip -o rclone.zip && unzip -jq rclone.zip '*/rclone' && chmod +x rclone
|
||||
# One JSON object per line, newest last. Served at
|
||||
# https://pkgs.omarchy.org/publish-log.jsonl
|
||||
./rclone copy R2:omarchy-pkgs/publish-log.jsonl . --s3-no-head 2>/dev/null || : > publish-log.jsonl
|
||||
jq -c . publish-record.json >> publish-log.jsonl
|
||||
./rclone copyto publish-log.jsonl R2:omarchy-pkgs/publish-log.jsonl --s3-no-head
|
||||
echo "log now has $(wc -l < publish-log.jsonl) entries"
|
||||
|
||||
- name: Comment on the merged PR
|
||||
# Only for a push: the merge commit names its PR. A dispatch runs
|
||||
# from master's head, whose PR merged something else entirely, so
|
||||
# commenting there would attach this run's report to the wrong PR.
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
pr=$(gh api "repos/${{ github.repository }}/commits/${{ github.sha }}/pulls" --jq '.[0].number // empty')
|
||||
if [[ -n "$pr" ]]; then
|
||||
gh pr comment "$pr" -R "${{ github.repository }}" --body-file comment.md
|
||||
echo "commented on #$pr"
|
||||
else
|
||||
echo "no PR for ${{ github.sha }} (manual dispatch?); skipping PR comment"
|
||||
fi
|
||||
result:
|
||||
needs: [changes, publish]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "changes result: ${{ needs.changes.result }}, publish result: ${{ needs.publish.result }}"
|
||||
[[ "${{ needs.changes.result }}" == "success" ]]
|
||||
[[ "${{ needs.publish.result }}" == "success" || "${{ needs.publish.result }}" == "skipped" ]]
|
||||
@@ -1,91 +0,0 @@
|
||||
name: Sync AUR Packages
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every 6 hours
|
||||
- cron: '0 */6 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
packages:
|
||||
description: 'Specific packages to sync (space-separated, leave empty for all)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Sync AUR packages
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e PACKAGES="$PACKAGES" \
|
||||
-e HOST_UID="$(id -u)" \
|
||||
-e HOST_GID="$(id -g)" \
|
||||
-v "$PWD/bin:/workspace/bin:ro" \
|
||||
-v "$PWD/helpers:/workspace/helpers:ro" \
|
||||
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
|
||||
-w /workspace \
|
||||
archlinux:base-devel bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
pacman -Syu --noconfirm git jq
|
||||
|
||||
groupadd -g "$HOST_GID" runner
|
||||
useradd -m -u "$HOST_UID" -g "$HOST_GID" runner
|
||||
chown -R runner:runner /workspace/pkgbuilds
|
||||
|
||||
if [[ -n "${PACKAGES:-}" ]]; then
|
||||
read -r -a package_args <<< "$PACKAGES"
|
||||
runuser -u runner -- ./bin/sync-aur "${package_args[@]}"
|
||||
else
|
||||
runuser -u runner -- ./bin/sync-aur
|
||||
fi
|
||||
'
|
||||
env:
|
||||
PACKAGES: ${{ github.event.inputs.packages }}
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
run: |
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: sync AUR packages'
|
||||
title: 'chore: sync AUR packages'
|
||||
body: |
|
||||
Automated AUR package sync.
|
||||
|
||||
Package sync behavior is controlled by `.omarchy/package.json`.
|
||||
branch: auto/sync-aur
|
||||
delete-branch: true
|
||||
labels: automated
|
||||
reviewers: ryanrhughes
|
||||
|
||||
- name: Notify Basecamp on failure
|
||||
if: failure() && env.BASECAMP_CHATBOT_URL != ''
|
||||
env:
|
||||
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
|
||||
run: |
|
||||
curl -s -o /dev/null \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg content \
|
||||
"🔴 <strong>AUR sync failed</strong><br><a href=\"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\">View run</a>" \
|
||||
'{content: $content}')" \
|
||||
"$BASECAMP_CHATBOT_URL"
|
||||
@@ -1,5 +1,14 @@
|
||||
name: Sync Rebuild Triggers
|
||||
|
||||
# Rebuilds ride the unattended lane, like track-branches.yml: the pkgrel bump
|
||||
# PR builds on the droplets, auto-merge lands it once `result`, `self-tests`
|
||||
# and `build-isolation` are green, and the merge publishes. A rebuild that
|
||||
# fails stays an unmerged red PR for a maintainer.
|
||||
#
|
||||
# The PR and auto-merge use the PAT in PKGS_BOT_TOKEN, because a merge made
|
||||
# with the built-in GITHUB_TOKEN does not start publish.yml, and its pushes
|
||||
# are held for approval instead of building.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every 6 hours, off the hour to dodge the scheduling backlog at :00
|
||||
@@ -19,11 +28,31 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Require the bot token
|
||||
env:
|
||||
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||
run: |
|
||||
if [[ -z "$PKGS_BOT_TOKEN" ]]; then
|
||||
echo "::error::Set PKGS_BOT_TOKEN to a PAT with Contents: write and Pull requests: write on this repository, owned by an account trusted to trigger builds."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# A scoped dispatch regenerates only the named packages. Pushed to the
|
||||
# shared branch, that would replace every other pending update in its
|
||||
# PR, so it gets a branch and PR of its own.
|
||||
- name: Choose the PR branch
|
||||
id: branch
|
||||
env:
|
||||
PACKAGES: ${{ github.event.inputs.packages }}
|
||||
run: |
|
||||
read -r -a package_args <<< "${PACKAGES:-}"
|
||||
.github/scripts/sync-pr-branch.sh auto/sync-rebuilds "${package_args[@]}" | tee -a "$GITHUB_OUTPUT"
|
||||
|
||||
# Runs in an Arch container against the mirror the x86_64 builder itself
|
||||
# uses, because the question being asked is what that builder will link
|
||||
# against and a different mirror can be hours ahead of it. Recording a
|
||||
@@ -70,11 +99,12 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||
commit-message: 'chore: rebuild against updated dependencies'
|
||||
title: 'chore: rebuild against updated dependencies'
|
||||
title: "chore: rebuild against updated dependencies${{ steps.branch.outputs.scope && format(' ({0})', steps.branch.outputs.scope) || '' }}"
|
||||
body: |
|
||||
Automated pkgrel bump for packages that link against a dependency
|
||||
which has moved in the official repositories.
|
||||
@@ -84,10 +114,27 @@ jobs:
|
||||
bump is what makes the rebuilt package an upgrade pacman will offer;
|
||||
without it the build produces the version already published and no
|
||||
one receives it.
|
||||
branch: auto/sync-rebuilds
|
||||
|
||||
This PR auto-merges once the build checks pass. A failing rebuild
|
||||
leaves it open for a maintainer.
|
||||
branch: ${{ steps.branch.outputs.branch }}
|
||||
delete-branch: true
|
||||
labels: automated
|
||||
reviewers: ryanrhughes
|
||||
|
||||
# Auto-merge, not a direct merge: branch protection still has to see
|
||||
# the build checks green before the rebuild lands.
|
||||
- name: Enable auto-merge
|
||||
if: steps.cpr.outputs.pull-request-number != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||
PR: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
run: |
|
||||
# Idempotent across re-runs of an updated PR: enabling twice errors.
|
||||
if [[ "$(gh pr view "$PR" -R "${{ github.repository }}" --json autoMergeRequest --jq '.autoMergeRequest != null')" == true ]]; then
|
||||
echo "auto-merge already enabled on #$PR"
|
||||
exit 0
|
||||
fi
|
||||
gh pr merge --auto --merge "$PR" -R "${{ github.repository }}"
|
||||
|
||||
- name: Notify Basecamp on failure
|
||||
if: failure() && env.BASECAMP_CHATBOT_URL != ''
|
||||
|
||||
@@ -17,6 +17,12 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
outputs:
|
||||
branch: ${{ steps.branch.outputs.branch }}
|
||||
pushed_at: ${{ steps.pushed.outputs.at }}
|
||||
number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
operation: ${{ steps.cpr.outputs.pull-request-operation }}
|
||||
head_sha: ${{ steps.cpr.outputs.pull-request-head-sha }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -24,12 +30,25 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# A scoped dispatch regenerates only the named packages. Pushed to the
|
||||
# shared branch, that would replace every other pending update in its
|
||||
# PR, so it gets a branch and PR of its own.
|
||||
- name: Choose the PR branch
|
||||
id: branch
|
||||
env:
|
||||
PACKAGES: ${{ github.event.inputs.packages }}
|
||||
run: |
|
||||
read -r -a package_args <<< "${PACKAGES:-}"
|
||||
.github/scripts/sync-pr-branch.sh auto/sync-upstream "${package_args[@]}" | tee -a "$GITHUB_OUTPUT"
|
||||
|
||||
# Runs in an Arch container for vercmp: whether a release is an upgrade has
|
||||
# to be decided by the same comparator pacman will use on users' machines.
|
||||
- name: Update packages from upstream release feeds
|
||||
id: sync
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e PACKAGES="$PACKAGES" \
|
||||
-e UPSTREAM_GITHUB_TOKEN="$UPSTREAM_GITHUB_TOKEN" \
|
||||
-e HOST_UID="$(id -u)" \
|
||||
-e HOST_GID="$(id -g)" \
|
||||
-v "$PWD/bin:/workspace/bin:ro" \
|
||||
@@ -39,23 +58,29 @@ jobs:
|
||||
archlinux:base-devel bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
pacman -Syu --noconfirm jq
|
||||
pacman -Syu --noconfirm git jq python libarchive
|
||||
|
||||
groupadd -g "$HOST_GID" runner
|
||||
useradd -m -u "$HOST_UID" -g "$HOST_GID" runner
|
||||
chown -R runner:runner /workspace/pkgbuilds
|
||||
|
||||
# The reviewed lane only: packages marked auto_merge ride
|
||||
# track-branches.yml, which merges without a human.
|
||||
if [[ -n "${PACKAGES:-}" ]]; then
|
||||
read -r -a package_args <<< "$PACKAGES"
|
||||
runuser -u runner -- ./bin/sync-upstream "${package_args[@]}"
|
||||
runuser -u runner -- ./bin/sync-upstream --lane reviewed "${package_args[@]}"
|
||||
else
|
||||
runuser -u runner -- ./bin/sync-upstream
|
||||
runuser -u runner -- ./bin/sync-upstream --lane reviewed
|
||||
fi
|
||||
'
|
||||
env:
|
||||
PACKAGES: ${{ github.event.inputs.packages }}
|
||||
UPSTREAM_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Failed feeds leave their recipes untouched; completed updates still
|
||||
# reach review. The failed sync step keeps the workflow red and notifies.
|
||||
- name: Check for changes
|
||||
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }}
|
||||
id: changes
|
||||
run: |
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
@@ -64,22 +89,35 @@ jobs:
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Runs created by this push are newer than this; the approve job
|
||||
# waits for them. A minute's slack absorbs runner clock skew.
|
||||
- name: Record push time
|
||||
if: ${{ !cancelled() && steps.changes.outputs.has_changes == 'true' }}
|
||||
id: pushed
|
||||
run: echo "at=$(date -u -d '-1 minute' +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
if: ${{ !cancelled() && steps.changes.outputs.has_changes == 'true' }}
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: sync upstream releases'
|
||||
title: 'chore: sync upstream releases'
|
||||
title: "chore: sync upstream releases${{ steps.branch.outputs.scope && format(' ({0})', steps.branch.outputs.scope) || '' }}"
|
||||
body: |
|
||||
Automated update of packages that track an upstream vendor release
|
||||
feed rather than the AUR.
|
||||
|
||||
Each package reports its newest release through
|
||||
`.omarchy/upstream.sh`.
|
||||
branch: auto/sync-upstream
|
||||
Release watches and providers are declared in `.omarchy/package.json`;
|
||||
exceptional feeds use `.omarchy/upstream.sh`. Failed package updates
|
||||
are left untouched; check the workflow result for outstanding failures.
|
||||
branch: ${{ steps.branch.outputs.branch }}
|
||||
delete-branch: true
|
||||
labels: automated
|
||||
# The bot is trusted; build-approved lets the approve job below
|
||||
# release GitHub's hold on its pushes without a maintainer.
|
||||
labels: |
|
||||
automated
|
||||
build-approved
|
||||
reviewers: ryanrhughes
|
||||
|
||||
- name: Notify Basecamp on failure
|
||||
@@ -93,3 +131,36 @@ jobs:
|
||||
"🔴 <strong>Upstream sync failed</strong><br><a href=\"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\">View run</a>" \
|
||||
'{content: $content}')" \
|
||||
"$BASECAMP_CHATBOT_URL"
|
||||
|
||||
# GitHub holds pull_request runs from a GITHUB_TOKEN push for approval and
|
||||
# creates no pull_request_target run for it, so approve-pr.yml never sees
|
||||
# the sync's own pushes. The sync labels its PR build-approved, so release
|
||||
# the held runs for the commit just pushed, whether it opened the PR or
|
||||
# updated it. A separate job, so the sync container's token never holds
|
||||
# actions: write.
|
||||
approve:
|
||||
needs: sync
|
||||
if: ${{ !cancelled() && (needs.sync.outputs.operation == 'created' || needs.sync.outputs.operation == 'updated') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Release held build and test runs
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
NUMBER: ${{ needs.sync.outputs.number }}
|
||||
BRANCH: ${{ needs.sync.outputs.branch }}
|
||||
HEAD_SHA: ${{ needs.sync.outputs.head_sha }}
|
||||
SINCE: ${{ needs.sync.outputs.pushed_at }}
|
||||
with:
|
||||
script: |
|
||||
const approve = require('./.github/scripts/approve-sync-push.cjs');
|
||||
const { NUMBER, BRANCH, HEAD_SHA, SINCE } = process.env;
|
||||
await approve({ github, context, core, number: Number(NUMBER),
|
||||
branch: BRANCH, headSha: HEAD_SHA, since: SINCE });
|
||||
@@ -1,12 +1,28 @@
|
||||
name: Tests
|
||||
|
||||
# PR-only. Publishing on push has its own workflow and is what verifies the
|
||||
# merged tree: it resolves every package against the live channel and refuses
|
||||
# a filename that already exists with different bytes, so two PRs cannot land
|
||||
# the same version twice. A post-merge test run would only repeat the PR's.
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-isolation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Prepare fixture builder
|
||||
run: docker build -t omarchy-build-isolation-test -f tests/build-isolation.Dockerfile tests
|
||||
- name: Verify isolated builds with real pacman transactions
|
||||
env:
|
||||
CONTAINER_ENGINE: docker
|
||||
TEST_BUILDER_IMAGE: omarchy-build-isolation-test
|
||||
run: tests/build-isolation.sh
|
||||
|
||||
self-tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -16,6 +32,12 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Test PR workflow approval
|
||||
run: node --test tests/pr-workflow-approval.cjs
|
||||
|
||||
- name: Test builder images
|
||||
run: node --test tests/builder-image.cjs
|
||||
|
||||
# An Arch container for vercmp: version ordering has to be decided by
|
||||
# the same comparator pacman uses on users' machines.
|
||||
- name: Run self-tests
|
||||
@@ -25,8 +47,27 @@ jobs:
|
||||
-w /workspace \
|
||||
archlinux:base-devel bash -lc '
|
||||
set -euo pipefail
|
||||
pacman -Syu --noconfirm jq
|
||||
pacman -Syu --noconfirm git jq python libarchive neovim tmux
|
||||
python tests/oma-service-removal.py
|
||||
python tests/upstream-watch.py
|
||||
bash tests/ipu7-install.sh
|
||||
bash tests/ipu7-headers.sh
|
||||
./bin/sync-upstream self-test
|
||||
./bin/sync-rebuilds --self-test
|
||||
./bin/omarchy-pkgs self-test
|
||||
./bin/omarchy-release self-test
|
||||
./tests/neovim-remote-clipboard.sh
|
||||
python tests/neovim-clipboard-tmux.py
|
||||
./tests/partial-release.sh
|
||||
./tests/published-build-plan.sh
|
||||
./tests/settings-boot-config.sh
|
||||
./tests/settings-runtime-profile.sh
|
||||
./tests/pinned-sources.sh
|
||||
./tests/controller.sh
|
||||
./tests/artifact-helpers.sh
|
||||
./tests/limine-mkinitcpio-hook.sh
|
||||
./tests/voxtype-bin-install.sh
|
||||
./tests/superwhisper-bin-install.sh
|
||||
pacman -S --noconfirm --quiet rclone >/dev/null
|
||||
./tests/publish-artifact.sh
|
||||
'
|
||||
@@ -0,0 +1,161 @@
|
||||
name: Track upstream branches
|
||||
|
||||
# The unattended lane. Packages marked "auto_merge": true follow a moving
|
||||
# upstream branch (omarchy-dev and omarchy-settings-dev on quattro, omasnap-git
|
||||
# on main) rather than tagged releases, so nothing in this repository changes
|
||||
# when their source does. This workflow makes each new branch tip a commit pin
|
||||
# in the recipe, which publish.yml then treats like any other version bump:
|
||||
# the PR builds on the droplets, auto-merge lands it when `result` is green,
|
||||
# and the merge publishes the artifacts. A tip that fails to build stays an
|
||||
# unmerged red PR that the next tick supersedes.
|
||||
#
|
||||
# The PR and auto-merge use the PAT in PKGS_BOT_TOKEN so they trigger the
|
||||
# build and publish workflows. The built-in GITHUB_TOKEN cannot drive this
|
||||
# unattended chain. The PAT needs Contents: write and Pull requests: write
|
||||
# on this repository, and its owner must be trusted by the build workflow.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every 2 hours, off the hour to dodge the scheduling backlog at :00
|
||||
- cron: '35 */2 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
packages:
|
||||
description: 'Specific packages to track (space-separated, leave empty for every auto_merge package)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
# One tracker at a time: two runs racing on auto/track-branches would each
|
||||
# force-push their own pin over the other's.
|
||||
concurrency:
|
||||
group: track-branches
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
track:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Require the tracking token
|
||||
env:
|
||||
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||
run: |
|
||||
if [[ -z "$PKGS_BOT_TOKEN" ]]; then
|
||||
echo "::error::Set PKGS_BOT_TOKEN to a PAT with Contents: write and Pull requests: write on this repository, owned by an account trusted to trigger builds."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Same container as the reviewed sync: vercmp decides whether a pin is
|
||||
# an upgrade with the comparator pacman uses on users' machines.
|
||||
- name: Pin tracked branches to their current tips
|
||||
id: sync
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e PACKAGES="$PACKAGES" \
|
||||
-e UPSTREAM_GITHUB_TOKEN="$UPSTREAM_GITHUB_TOKEN" \
|
||||
-e HOST_UID="$(id -u)" \
|
||||
-e HOST_GID="$(id -g)" \
|
||||
-v "$PWD/bin:/workspace/bin:ro" \
|
||||
-v "$PWD/helpers:/workspace/helpers:ro" \
|
||||
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
|
||||
-w /workspace \
|
||||
archlinux:base-devel bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
pacman -Syu --noconfirm git jq python libarchive
|
||||
|
||||
groupadd -g "$HOST_GID" runner
|
||||
useradd -m -u "$HOST_UID" -g "$HOST_GID" runner
|
||||
chown -R runner:runner /workspace/pkgbuilds
|
||||
|
||||
if [[ -n "${PACKAGES:-}" ]]; then
|
||||
read -r -a package_args <<< "$PACKAGES"
|
||||
runuser -u runner -- ./bin/sync-upstream --lane auto-merge "${package_args[@]}"
|
||||
else
|
||||
runuser -u runner -- ./bin/sync-upstream --lane auto-merge
|
||||
fi
|
||||
'
|
||||
env:
|
||||
PACKAGES: ${{ github.event.inputs.packages }}
|
||||
UPSTREAM_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check for changes
|
||||
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }}
|
||||
id: changes
|
||||
run: |
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
git status --porcelain
|
||||
{
|
||||
echo "### Pinned"
|
||||
git diff --unified=0 -- 'pkgbuilds/*/PKGBUILD' | grep -E '^\+(pkgver|_commit)=' | sed 's/^+/- /'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
# The PR title names what moved, so the merged history reads like a
|
||||
# changelog: "Track upstream branches: omarchy-dev 4.0.0.r6520.g1a2b3c4, ...".
|
||||
- name: Describe the pins
|
||||
if: ${{ !cancelled() && steps.changes.outputs.has_changes == 'true' }}
|
||||
id: describe
|
||||
run: |
|
||||
title=$(git diff --unified=0 -- 'pkgbuilds/*/PKGBUILD' \
|
||||
| awk '/^\+\+\+ b\/pkgbuilds\//{split($2,p,"/"); pkg=p[3]} /^\+pkgver=/{sub(/^\+pkgver=/,""); printf "%s %s, ", pkg, $0}' \
|
||||
| sed 's/, $//')
|
||||
echo "title=Track upstream branches: ${title}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Open or update the tracking PR
|
||||
if: ${{ !cancelled() && steps.changes.outputs.has_changes == 'true' }}
|
||||
id: pr
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
token: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||
commit-message: ${{ steps.describe.outputs.title }}
|
||||
title: ${{ steps.describe.outputs.title }}
|
||||
body: |
|
||||
Automated pin of packages that follow a moving upstream branch
|
||||
(`"auto_merge": true` in `.omarchy/package.json`). Each package's
|
||||
`_commit` now points at the branch tip. Fresh tips wait until
|
||||
their commit timestamp is at least `min_release_age` old.
|
||||
|
||||
This PR auto-merges once the build checks pass. A failing build
|
||||
leaves it open; the next tracker run replaces it with the newer tip.
|
||||
branch: auto/track-branches
|
||||
delete-branch: true
|
||||
labels: automated
|
||||
|
||||
# Auto-merge, not a direct merge: branch protection still has to see
|
||||
# `result`, `self-tests` and `build-isolation` green, and this lane
|
||||
# inherits every rule the reviewed lane has except the human.
|
||||
- name: Enable auto-merge
|
||||
if: ${{ !cancelled() && steps.pr.outputs.pull-request-number != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||
PR: ${{ steps.pr.outputs.pull-request-number }}
|
||||
run: |
|
||||
# Idempotent across re-runs of an updated PR: enabling twice errors.
|
||||
if [[ "$(gh pr view "$PR" -R "${{ github.repository }}" --json autoMergeRequest --jq '.autoMergeRequest != null')" == true ]]; then
|
||||
echo "auto-merge already enabled on #$PR"
|
||||
exit 0
|
||||
fi
|
||||
gh pr merge --auto --merge "$PR" -R "${{ github.repository }}"
|
||||
|
||||
- name: Notify Basecamp on failure
|
||||
if: failure() && env.BASECAMP_CHATBOT_URL != ''
|
||||
env:
|
||||
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
|
||||
run: |
|
||||
curl -s -o /dev/null \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg content \
|
||||
"🔴 <strong>Branch tracking failed</strong><br><a href=\"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\">View run</a>" \
|
||||
'{content: $content}')" \
|
||||
"$BASECAMP_CHATBOT_URL"
|
||||
@@ -0,0 +1,79 @@
|
||||
name: Unpublish retired packages
|
||||
|
||||
# Takes packages out of the channel databases after their recipes are gone
|
||||
# from master. Deleting a recipe stops it building; this stops pacman
|
||||
# offering it. Files stay in the bucket (see bin/unpublish-packages).
|
||||
#
|
||||
# Only packages with no recipe on master: one that still has a recipe would
|
||||
# come back on its next publish, and refusing it keeps a typo from pulling a
|
||||
# live package out of every channel.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
packages:
|
||||
description: "Space-separated pkgbases whose recipes were removed from master"
|
||||
required: true
|
||||
channels:
|
||||
description: "Channels to remove them from"
|
||||
required: true
|
||||
default: "edge rc stable"
|
||||
|
||||
jobs:
|
||||
unpublish:
|
||||
runs-on: ubuntu-latest
|
||||
environment: publish
|
||||
timeout-minutes: 15
|
||||
# The publish job's group: one writer per channel database at a time.
|
||||
concurrency:
|
||||
group: publish
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Refuse packages that still have a recipe
|
||||
env:
|
||||
PACKAGES: ${{ github.event.inputs.packages }}
|
||||
CHANNELS: ${{ github.event.inputs.channels }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for c in $CHANNELS; do
|
||||
[[ $c == edge || $c == rc || $c == stable ]] || { echo "::error::Unknown channel: $c"; exit 1; }
|
||||
done
|
||||
for p in $PACKAGES; do
|
||||
[[ $p =~ ^[a-z0-9@._+-]+$ ]] || { echo "::error::Not a package name: $p"; exit 1; }
|
||||
if [[ -e pkgbuilds/$p ]]; then
|
||||
echo "::error::pkgbuilds/$p still exists on master; remove the recipe first"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Unpublish
|
||||
env:
|
||||
PACKAGES: ${{ github.event.inputs.packages }}
|
||||
CHANNELS: ${{ github.event.inputs.channels }}
|
||||
RCLONE_CONFIG_R2_TYPE: s3
|
||||
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
|
||||
RCLONE_CONFIG_R2_NO_CHECK_BUCKET: "true"
|
||||
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
||||
OMARCHY_PUBLISH_PREFIX: ${{ vars.OMARCHY_PUBLISH_PREFIX }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# repo-remove and bsdtar are Arch tools: run in the tested builder
|
||||
# image, as the publish job does.
|
||||
builder=ghcr.io/omacom/omarchy-pkg-builder:$(bin/builder-image key --arch x86_64 --mirror edge)
|
||||
docker pull --quiet "$builder"
|
||||
for mirror in $CHANNELS; do
|
||||
for arch in x86_64 aarch64; do
|
||||
docker run --rm \
|
||||
-e OMARCHY_PUBLISH_PREFIX \
|
||||
-e RCLONE_CONFIG_R2_TYPE -e RCLONE_CONFIG_R2_PROVIDER -e RCLONE_CONFIG_R2_ENDPOINT -e RCLONE_CONFIG_R2_NO_CHECK_BUCKET \
|
||||
-e RCLONE_CONFIG_R2_ACCESS_KEY_ID -e RCLONE_CONFIG_R2_SECRET_ACCESS_KEY \
|
||||
-v "$PWD:/w:ro" -w /w "$builder" \
|
||||
bin/unpublish-packages --remote R2:omarchy-pkgs --mirror "$mirror" --arch "$arch" $PACKAGES
|
||||
done
|
||||
done
|
||||
@@ -1,6 +1,8 @@
|
||||
.firecrawl/
|
||||
src/
|
||||
logs/
|
||||
build-output/
|
||||
cache/
|
||||
pkgs.omarchy.org/
|
||||
pkgbuilds/*/.SRCINFO
|
||||
pkgbuilds/*/.gitignore
|
||||
@@ -35,3 +37,7 @@ pkgbuilds/yay/yay/
|
||||
.srcdest/
|
||||
.repo-host
|
||||
.worktrees/
|
||||
|
||||
# Python helpers and offline tests
|
||||
__pycache__/
|
||||
.release-verification/
|
||||
@@ -23,25 +23,72 @@ The filesystem no longer encodes release policy. Instead:
|
||||
(`OMARCHY_RC_PINS=1`, which `omarchy-release rc` sets) may build it for rc — master's
|
||||
shipped pins can never overwrite an in-flight RC. The dev pair
|
||||
(`omarchy-dev`, `omarchy-settings-dev`) is pinned to `edge`
|
||||
- AUR sync behavior is controlled by `source`, `sync`, `aur`, patches, and hooks in `.omarchy/`
|
||||
- packages that follow a moving upstream branch (the dev pair on `quattro`,
|
||||
`omasnap-git` on `main`) still pin an exact commit in their PKGBUILD. A
|
||||
`git_branch` upstream watch moves that pin, and `"auto_merge": true` puts the
|
||||
package on the unattended lane: `track-branches.yml` opens the bump PR every
|
||||
two hours and auto-merges it once the build checks pass, so a branch tip
|
||||
reaches the edge channel without anyone clicking. No PKGBUILD may carry an
|
||||
unpinned git source (`tests/pinned-sources.sh`); a branch that has to be
|
||||
followed gets a watch, not a `#branch=` fragment
|
||||
- Omarchy owns every checked-in recipe; upstream watches update release metadata without replacing packaging or architecture support
|
||||
- packages can opt out of unscoped builds with `skip_build`; explicit `--package` builds remain available
|
||||
- packages that follow a vendor release feed instead of the AUR carry an `.omarchy/upstream.sh` hook
|
||||
- packages follow direct upstream watches/providers in `.omarchy/package.json`, or a custom `.omarchy/upstream.sh` hook
|
||||
|
||||
## Prerequisites
|
||||
### aarch64 Builds (Optional)
|
||||
|
||||
To build ARM64 packages on x86_64, enable QEMU emulation:
|
||||
The repository host builds every architecture it publishes on the same
|
||||
machine. A foreign architecture runs under QEMU user emulation, which
|
||||
`bin/build` checks by actually running a container for the target platform.
|
||||
Rootful Docker registers QEMU on first use. Rootless Podman uses the host's
|
||||
registration and prints the one-time Arch setup commands when it is missing or
|
||||
lacks the credential flag required by `sudo` inside the builder:
|
||||
|
||||
```bash
|
||||
# Run after each reboot
|
||||
docker run --privileged --rm tonistiigi/binfmt --install arm64
|
||||
|
||||
# Verify
|
||||
docker run --rm --platform linux/arm64 alpine:latest uname -m
|
||||
podman run --rm --platform linux/arm64 docker.io/library/alpine:latest uname -m
|
||||
# Should output: aarch64
|
||||
```
|
||||
|
||||
**Note**: aarch64 builds use QEMU and slower than native x86_64 builds.
|
||||
**Note**: emulated builds are much slower than native ones.
|
||||
|
||||
### Published architectures
|
||||
|
||||
`helpers/paths.sh` names the architectures this repository publishes:
|
||||
|
||||
```bash
|
||||
PUBLISHED_ARCHES="${OMARCHY_ARCHES:-x86_64}"
|
||||
```
|
||||
|
||||
That list drives the whole scheduled pipeline. `check-versions` compares
|
||||
PKGBUILDs against each architecture's channel databases and writes one queue
|
||||
file per channel and architecture (`.sync-needed-<channel>-<arch>`);
|
||||
`auto-release <channel>` works through the queues one architecture at a
|
||||
time, each with its own backoff (`.build-failed-<channel>-<arch>`), so a
|
||||
failing build on one architecture never holds up the other; and the release
|
||||
train advances channels with `--arch all`: it takes one host-wide lock and
|
||||
verifies every architecture's source database before moving any of them. The
|
||||
first entry is the reference architecture the release train observes channels
|
||||
through. A remote sync failure can still leave a promotion temporarily partial;
|
||||
rerunning the same advance completes it safely.
|
||||
|
||||
Adding an architecture to the scheduled pipeline is therefore one checked-in
|
||||
change to that list: the next `check-versions` tick queues everything the new
|
||||
architecture lacks, and the next `auto-release` tick starts building it. A
|
||||
checked-in list also means the rebuild workflow and release host cannot drift
|
||||
onto different architecture sets. For a one-off run, override it directly:
|
||||
|
||||
```bash
|
||||
OMARCHY_ARCHES=x86_64 bin/check-versions
|
||||
OMARCHY_ARCHES=aarch64 bin/check-versions
|
||||
OMARCHY_ARCHES="x86_64 aarch64" bin/check-versions
|
||||
```
|
||||
|
||||
The builder image bootstraps
|
||||
`omarchy-keyring` from the x86_64 tree for every architecture, so the first
|
||||
build of a new architecture does not depend on a repository that only it can
|
||||
create.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -86,6 +133,14 @@ bin/repo advance --from edge --to rc
|
||||
|
||||
The release command is smart and **incremental** - it only builds packages that have changed or are missing. You generally don't need to specify a package manually unless you are debugging a specific failure.
|
||||
|
||||
When a package fails, a completed build run still signs and publishes the packages
|
||||
that succeeded. Failed packages and their blocked dependents remain queued with
|
||||
failure backoff; retries compare against the updated repository and skip the
|
||||
published versions. Only artifacts recorded by fully completed package builds
|
||||
are eligible for a partial release. An interrupted build, a failed publication
|
||||
step, or an incomplete pair using deferred runtime dependencies still stops the
|
||||
release. Reports distinguish partial publication from complete success.
|
||||
|
||||
```bash
|
||||
# Build changed/new packages, sign, promote, clean, update, and sync
|
||||
bin/repo release
|
||||
@@ -282,14 +337,16 @@ push uploaded, so `push` stops when it finds packages already staged there —
|
||||
usually leftovers from a failed run. Remove them on the host, or pass
|
||||
`--include-staged` to publish them too.
|
||||
|
||||
### Sync AUR PKGBUILDs
|
||||
### Import an initial AUR recipe
|
||||
|
||||
```bash
|
||||
bin/sync-aur # Sync all AUR packages with sync enabled
|
||||
bin/sync-aur yay v4l2-relayd # Sync specific packages
|
||||
bin/add-package package-name --source aur
|
||||
```
|
||||
|
||||
AUR sync is metadata-driven. It preserves `.omarchy/`, replaces the package root with AUR contents, applies `.omarchy/patches/*.patch`, runs `.omarchy/post-sync.sh` when present, applies pkgrel metadata, removes AUR-only `.SRCINFO` and `.gitignore` files, and records `upstream_commit`.
|
||||
AUR is an optional source for an initial recipe. Imported packages become
|
||||
Omarchy-owned immediately; subsequent updates use direct upstream releases.
|
||||
There is no scheduled AUR sync. Edit the checked-in PKGBUILD to maintain
|
||||
architecture support and packaging behavior.
|
||||
|
||||
### Sync Upstream Releases
|
||||
|
||||
@@ -300,10 +357,11 @@ bin/sync-upstream openai-codex-desktop # Update specific packages
|
||||
|
||||
Some vendors publish a release feed of their own that is faster and more precise
|
||||
than the AUR packaging of it. Those packages are `source: local` — Omarchy owns
|
||||
the PKGBUILD — and declare where releases come from in one of two ways.
|
||||
the PKGBUILD — and declare where releases come from either as data or, for an
|
||||
unusual feed, a small hook.
|
||||
|
||||
A vendor shipping tagged GitHub releases with a checksum manifest asset is pure
|
||||
data, declared as `upstream` in `.omarchy/package.json` with no code at all:
|
||||
A vendor shipping tagged GitHub releases is pure data, declared as `upstream`
|
||||
in `.omarchy/package.json` with no code at all:
|
||||
|
||||
```json
|
||||
"upstream": {
|
||||
@@ -316,16 +374,107 @@ data, declared as `upstream` in `.omarchy/package.json` with no code at all:
|
||||
}
|
||||
```
|
||||
|
||||
`checksums` names the manifest asset the vendor publishes. A vendor publishing
|
||||
none sets `"digests": true` instead, and the checksums come from the SHA-256
|
||||
digest GitHub's release API reports for every asset — see
|
||||
`pkgbuilds/schist-bin/.omarchy/package.json`. Either way the artifacts
|
||||
themselves are never downloaded.
|
||||
|
||||
An architecture may map to an ordered array when its PKGBUILD downloads more
|
||||
than one release asset. Small versioned files outside the release assets can be
|
||||
listed under `sources` and are downloaded and hashed when a new version appears:
|
||||
|
||||
```json
|
||||
"upstream": {
|
||||
"github": "owner/project",
|
||||
"digests": true,
|
||||
"assets": {
|
||||
"x86_64": ["tool-{pkgver}-x86_64", "tool-{pkgver}-x86_64.asc"],
|
||||
"aarch64": ["tool-{pkgver}-aarch64", "tool-{pkgver}-aarch64.asc"]
|
||||
},
|
||||
"sources": {
|
||||
"any": ["https://raw.githubusercontent.com/owner/project/{tag}/LICENSE"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Asset and source keys must be disjoint because each key maps to one PKGBUILD
|
||||
checksum array (`any` means the unsuffixed `sha256sums`).
|
||||
|
||||
Repositories whose historical releases use incompatible tag schemes may set
|
||||
`"latest_only": true`. The provider then considers only the newest stable
|
||||
GitHub release, while retaining all validation for that release. A quarantine
|
||||
will wait for that release to age instead of falling back to an older one.
|
||||
|
||||
`{tag}` and `{pkgver}` interpolate into asset names; a leading `v` on the tag is
|
||||
stripped for `pkgver`; drafts and prereleases are ignored. Only the 100 most
|
||||
recent releases are considered. The provider fails closed on anything it cannot
|
||||
read — an unusable tag, timestamp, or checksum stops the sync rather than being
|
||||
skipped.
|
||||
|
||||
A package may also declare `"min_release_age": "24h"` (`s`/`m`/`h`/`d` suffix or
|
||||
bare seconds) to quarantine fresh releases until maintainers have had time to
|
||||
pull a bad or compromised one. The newest release that has cleared the window
|
||||
ships, so a fast release cadence cannot starve updates. The window is enforced
|
||||
Projects that publish version tags but no checksum manifest can declare the
|
||||
tag repository, the exact tag shape, and every source that should be hashed:
|
||||
|
||||
```json
|
||||
"upstream": {
|
||||
"git_tags": "https://github.com/owner/project.git",
|
||||
"tag_pattern": "v{pkgver}",
|
||||
"sources": {
|
||||
"any": ["https://github.com/owner/project/archive/refs/tags/{tag}.tar.gz"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The newest matching tag is selected with pacman's `vercmp`; unrelated tags are
|
||||
ignored. `tag_pattern` must contain exactly one `{pkgver}`. Source templates may
|
||||
use `{tag}` and `{pkgver}`. Each expanded URL must be HTTPS and is downloaded
|
||||
only when the discovered version is newer. A checked-in patch or other local
|
||||
source can be included as `file:patch-name.patch`; it is hashed from the package
|
||||
directory. Keys such as `any`, `x86_64`, and `aarch64` select the corresponding
|
||||
`sha256sums` array.
|
||||
|
||||
npm packages use the same source mapping, with `{npm_tarball}` available for
|
||||
the tarball named by the selected dist-tag:
|
||||
|
||||
```json
|
||||
"upstream": {
|
||||
"npm": "@scope/package",
|
||||
"dist_tag": "latest",
|
||||
"sources": {
|
||||
"any": ["{npm_tarball}", "https://example.com/v{pkgver}/CHANGELOG.md"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`dist_tag` defaults to `latest`. The registry's publication timestamp is
|
||||
carried into the provider result, so `min_release_age` works for npm packages.
|
||||
|
||||
A vendor with a plain-text Debian `Packages` index can use it to discover the
|
||||
newest exact package version, then hash immutable source URLs:
|
||||
|
||||
```json
|
||||
"upstream": {
|
||||
"debian": "https://example.com/debian/dists/stable/main/binary-amd64/Packages",
|
||||
"package": "example-app",
|
||||
"sources": {
|
||||
"x86_64": ["https://example.com/tool-{pkgver}-x64.tar.gz"],
|
||||
"aarch64": ["https://example.com/tool-{pkgver}-arm64.tar.gz"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This deliberately accepts only Debian versions that are already valid Arch
|
||||
`pkgver` values. Feeds needing epoch, revision, or filename translation retain
|
||||
a hook. Exactly one of `github`, `git_tags`, `npm`, or `debian` may appear in a
|
||||
declaration.
|
||||
|
||||
A timestamped provider may also declare `"min_release_age": "24h"`
|
||||
(`s`/`m`/`h`/`d` suffix or bare seconds) to quarantine fresh releases until
|
||||
maintainers have had time to pull a bad or compromised one. GitHub Releases and
|
||||
npm provide publication times; raw git tags and Debian Packages indexes do not,
|
||||
so combining either with this policy fails closed. The newest release that has
|
||||
cleared the window ships, so a fast release cadence cannot starve updates. The
|
||||
window is enforced
|
||||
centrally: whatever reports the release must prove its age via `published_at`,
|
||||
or the sync fails. A maintainer deliberately shipping inside the window runs
|
||||
`BYPASS_MIN_RELEASE_AGE=1 bin/sync-upstream <package>` locally and merges the
|
||||
@@ -378,7 +527,13 @@ A package names those dependencies in `.omarchy/package.json`:
|
||||
{ "source": "aur", "sync": false, "rebuild_on": ["qt6-base", "qt6-declarative", "qt6-wayland"] }
|
||||
```
|
||||
|
||||
`bin/sync-rebuilds` reads each named package's version from the official repositories and compares it to `rebuilt_against`, the record of what the checked-in pkgrel was last bumped for. pkgrel is bumped unless every name in `rebuild_on` is recorded and still matches, so a name the record does not carry reads as changed rather than going unexamined forever. Opting a package in therefore buys one rebuild: what its published build actually linked against is not knowable from here, and a record written without a rebuild would certify a build nobody checked.
|
||||
`bin/sync-rebuilds` reads each named package's version from the official
|
||||
repositories for every architecture a merge builds (`CI_ARCHES` in
|
||||
`helpers/paths.sh`, both by default) that the package supports and compares
|
||||
it to `rebuilt_against`. Records are kept per architecture because Arch and
|
||||
Arch Linux ARM can carry different dependency versions. pkgrel is bumped once
|
||||
when any recorded version moves; that one source revision is then rebuilt by
|
||||
each architecture's normal queue.
|
||||
|
||||
The bump is the point of the command, and it has to land in git rather than in the builder. A rebuild that reuses the published version string produces a package pacman will never offer anyone, so merely unlocking the build gate would ship nothing. Bumping pkgrel needs no other change: `bin/check-versions` and the builder both already rebuild when pkgrel moves.
|
||||
|
||||
@@ -386,9 +541,14 @@ For an AUR-synced package the bump is expressed as the dotted Omarchy pkgrel suf
|
||||
|
||||
The bumped version is checked against the published one as well as the checked-in one, and refused when pacman would not order it higher. The checked-in version is not the floor; what a user already has is, and a checkout that has fallen behind the repository can otherwise be bumped to something that loses to the package it means to replace. That check is skipped with a warning when the published database cannot be read.
|
||||
|
||||
Versions are read from the local pacman database, so this runs on Arch or in an Arch container against a synced database. Only `core`, `extra` and `multilib` count: a Qt release sitting in testing or kde-unstable is not what the builder will link against, and rebuilding for it would ship a package built against the wrong ABI. The workflow points that database at `mirror.omarchy.org`, the mirror the x86_64 builder itself uses, because a mirror running ahead of the builder would record a version the build never linked against and nothing re-fires once the record matches.
|
||||
x86_64 versions are read from the local pacman database, so the workflow runs
|
||||
in an Arch container pointed at `mirror.omarchy.org`, the same mirror as the
|
||||
x86_64 builder. aarch64 versions are read directly from the live Arch Linux ARM
|
||||
repository database, which is also what the ARM builder uses. Testing and
|
||||
staging repositories do not count. A legacy flat `rebuilt_against` record is
|
||||
read as x86_64 and is migrated naturally the next time a rebuild is needed.
|
||||
|
||||
aarch64 is not covered. Those builds resolve Qt from Arch Linux ARM, which can lag Arch, so one record cannot describe both architectures. Only x86_64 is published today, so nothing currently ships from the untracked side; if ARM publishing starts, `rebuilt_against` has to become per-architecture before this can be trusted there.
|
||||
A dependency this repository carries for an architecture (a recipe here that builds for edge on it, such as aquamarine on aarch64) shadows the distribution's, because the builder lists `[omarchy]` first. Its version is the recipe's, and it counts only once edge publishes that version: until then the builder still links against the previous one, so dependents are left alone for that run.
|
||||
|
||||
### Other
|
||||
|
||||
@@ -403,14 +563,28 @@ bin/omarchy-release # Release front door (start / pick / rc / s
|
||||
bin/repo list # List package metadata
|
||||
bin/repo deploy # Build locally, then publish from the host
|
||||
bin/repo push # Upload local builds to the host and publish
|
||||
bin/add-package <package> # Add an AUR/local package with metadata
|
||||
bin/package-worktree <package> # Create upstream/patched/current scratch workspace
|
||||
bin/repo remove <package> # Remove package
|
||||
bin/add-package <package> # Add an Omarchy-owned package with metadata
|
||||
bin/package-worktree <package> # Inspect historical AUR provenance in a scratch workspace
|
||||
bin/repo remove <package> # Remove package (host workflow; CI uses unpublish.yml)
|
||||
bin/sync-upstream # Update packages that track a vendor release feed
|
||||
bin/sync-rebuilds # Bump pkgrel for packages whose dependencies moved
|
||||
bin/clean-docker # Clear Docker images/cache (forces fresh rebuild)
|
||||
```
|
||||
|
||||
### Retiring a package
|
||||
|
||||
Delete its recipe directory in a PR. Once that merges, take it out of the
|
||||
channel databases with the **Unpublish retired packages** workflow:
|
||||
|
||||
```
|
||||
gh workflow run unpublish.yml -f packages="<pkgbase> ..." -f channels="edge rc stable"
|
||||
```
|
||||
|
||||
It removes every entry built from those pkgbases (split outputs and `-debug`
|
||||
included) from both architectures' databases, and refuses any package that
|
||||
still has a recipe on master. Package files stay in the bucket: published
|
||||
filenames are immutable, and nothing references them once the entries are gone.
|
||||
|
||||
### Package Metadata Tools
|
||||
|
||||
```bash
|
||||
@@ -423,7 +597,7 @@ bin/repo list # Table view of source package metadata
|
||||
bin/repo list --json # Agent/script-friendly JSON
|
||||
bin/repo list --repo --mirror stable # List packages in a published repo database
|
||||
|
||||
bin/package-worktree v4l2-relayd # Create upstream/patched/current scratch workspace
|
||||
bin/package-worktree yay # Compare with the original imported AUR recipe
|
||||
```
|
||||
|
||||
## Cutting an Omarchy Release
|
||||
@@ -537,9 +711,7 @@ omarchy-pkgs/
|
||||
│ ├── PKGBUILD
|
||||
│ └── .omarchy/
|
||||
│ ├── package.json # Source/sync/release metadata
|
||||
│ ├── patches/ # Omarchy patches reapplied after AUR sync
|
||||
│ ├── post-sync.sh # Optional dynamic post-sync customization hook
|
||||
│ └── upstream.sh # Optional vendor release feed hook (non-AUR packages)
|
||||
│ └── upstream.sh # Optional custom vendor release feed hook
|
||||
├── build/
|
||||
├── build-output/ # Unsigned packages (temporary)
|
||||
│ ├── edge/ # (rc/ and stable/ alongside, each x86_64 + aarch64)
|
||||
@@ -559,45 +731,29 @@ Each source package has Omarchy metadata at `pkgbuilds/<package>/.omarchy/packag
|
||||
|
||||
Minimal examples:
|
||||
|
||||
```json
|
||||
{ "source": "aur" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "source": "aur", "sync": false }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "source": "aur", "release_ring": "fast" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "source": "local" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "source": "local", "release_ring": "fast" }
|
||||
{ "source": "local", "skip_build": true }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "source": "aur", "pkgrel": { "suffix": 1 } }
|
||||
{ "source": "local", "upstream": { "watch": { "github": "abenz1267/walker", "pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)" } } }
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `source`: `aur` or `local`. A `local` package can still follow an upstream release, either declaratively via `upstream` or with an `.omarchy/upstream.sh` hook.
|
||||
- `upstream`: optional for `local` packages whose vendor ships tagged GitHub releases with a checksum manifest asset. `{ "github": "owner/repo", "checksums": "SHASUMS256.txt", "assets": { "<arch>": "name-{tag}.tar.xz" } }` — see [Sync Upstream Releases](#sync-upstream-releases). Mutually exclusive with `.omarchy/upstream.sh`.
|
||||
- `source`: `local` for maintained packages. The legacy `aur` value is used only during an initial import. A local recipe can follow an upstream watch, provider, or `.omarchy/upstream.sh` hook.
|
||||
- `upstream`: optional direct release watch (see [Upstream watches](docs/upstream-sources.md)), or an existing GitHub, git-tag, npm, or Debian provider. GitHub architecture assets may be a string or an ordered array, and can be combined with disjoint versioned `sources` — see [Sync Upstream Releases](#sync-upstream-releases). Mutually exclusive with `.omarchy/upstream.sh`.
|
||||
- `min_release_age`: optional quarantine for upstream releases (`"24h"`, `"2d"`, or bare seconds). The newest release older than the window ships; anything younger waits, and a release whose age cannot be proven fails the sync. Bypass deliberately with `BYPASS_MIN_RELEASE_AGE=1 bin/sync-upstream <package>`.
|
||||
- `sync`: optional for AUR packages; defaults to `true`. Set `false` for AUR-origin packages that Omarchy maintains manually.
|
||||
- `aur`: optional AUR package name when it differs from the local package directory, usually for split packages.
|
||||
- `sync`: `false` records an existing manual maintenance hold. Held packages have no upstream watch/provider/hook and are excluded from automatic updates.
|
||||
- `auto_merge`: optional boolean; defaults to `false`. `true` moves the package's upstream updates from the reviewed 6-hourly sync PR to the unattended lane: `track-branches.yml` opens its bump PR and auto-merges it when CI is green. Meant for packages that follow a moving branch through a `git_branch` watch, where every tip is a release and there is nothing for a reviewer to read. Requires an upstream watch, provider, or hook.
|
||||
- `origin`: optional historical import provenance, with `aur` (package name) and `commit`. It does not control updates.
|
||||
- `release_ring`: optional. `fast` means the package is built directly for stable as well as edge, with the artifacts replicated into rc for parity. Packages without a ring build in edge and reach stable through the pipeline (`bin/repo advance`).
|
||||
- `channels`: optional array bounding where the package may be built (`edge`, `rc`, `stable`). Without the key a package is a member of every channel and follows the default build rules above; `bin/repo advance` refuses to carry a package anywhere it isn't a member.
|
||||
- `pinned`: optional boolean. A pinned package's version is set per release by `omarchy-release` on the `rc` branch, so it is never built for stable (promotion only) and is built for rc only from that branch's worktree (`OMARCHY_RC_PINS=1`). Used by `omarchy` and `omarchy-settings`.
|
||||
- `skip_build`: optional boolean; defaults to `false`. Set `true` to exclude a package from scheduled version checks and unscoped builds. The package can still be built explicitly with `bin/repo release --package <name>`.
|
||||
- `pkgrel`: optional Omarchy pkgrel suffix for a version-pinned rebuild bump. This emits `<aur pkgrel>.<suffix>` instead of replacing AUR's pkgrel. `offset` can be used only when preserving monotonic upgrades from old absolute pkgrel bumps. The metadata is removed automatically when AUR sync changes `pkgver`; the current package version is read from the checked-in PKGBUILD, so the version is not duplicated in JSON.
|
||||
- `pkgrel`: legacy import customization metadata. Maintained recipes keep their complete package release directly in PKGBUILD; rebuilds increment it there.
|
||||
- `rebuild_on`: optional array of package names this package links against closely enough that it must be rebuilt when they change, independent of its own source. Read by `bin/sync-rebuilds`.
|
||||
- `rebuilt_against`: written by `bin/sync-rebuilds`. Records the version of each `rebuild_on` package that the current pkgrel was bumped for.
|
||||
- `upstream_commit`: set by `bin/sync-aur` for AUR packages. Used by `bin/package-worktree` to recreate the exact raw AUR package that Omarchy last synced.
|
||||
- `rebuilt_against`: written by `bin/sync-rebuilds`. Maps each built architecture to the versions of its `rebuild_on` packages that the current pkgrel was bumped for.
|
||||
- `upstream_commit`: legacy AUR metadata, superseded by `origin.commit`. `bin/package-worktree` can use historical provenance to inspect the original recipe.
|
||||
|
||||
### Build Matrix
|
||||
|
||||
@@ -609,78 +765,26 @@ Fields:
|
||||
|
||||
## Adding Packages
|
||||
|
||||
### From AUR
|
||||
### Start from an existing recipe
|
||||
|
||||
```bash
|
||||
bin/add-package package-name
|
||||
bin/add-package package-name --source aur --fast
|
||||
# Review the imported files, own any architecture/packaging changes directly,
|
||||
# and declare an upstream watch/provider or hook in .omarchy/.
|
||||
bin/sync-upstream package-name
|
||||
bin/repo release --package package-name
|
||||
```
|
||||
|
||||
### From AUR, fast release ring
|
||||
The import records historical provenance in `origin`. It does not opt a package
|
||||
into future AUR imports. Upstream watches update only release scalars and source
|
||||
checksums; downstream build behavior stays in the recipe. Ordinary source-code
|
||||
patches still belong beside PKGBUILD and are applied by `prepare()` as needed.
|
||||
|
||||
### Custom package
|
||||
|
||||
```bash
|
||||
bin/add-package package-name --fast
|
||||
bin/repo release --package package-name
|
||||
bin/repo release --mirror stable --package package-name
|
||||
```
|
||||
|
||||
### AUR-origin, manually maintained by Omarchy
|
||||
|
||||
```bash
|
||||
bin/add-package package-name --no-sync
|
||||
```
|
||||
|
||||
### Local Customizations for AUR Packages
|
||||
|
||||
For static changes, create `pkgbuilds/package-name/.omarchy/patches/*.patch` to maintain modifications across AUR syncs.
|
||||
|
||||
The recommended workflow is to use a scratch workspace:
|
||||
|
||||
```bash
|
||||
bin/package-worktree package-name --dir /tmp/package-name-worktree
|
||||
```
|
||||
|
||||
This creates:
|
||||
|
||||
```text
|
||||
upstream/ # raw AUR package at upstream_commit
|
||||
patched/ # AUR + existing Omarchy .omarchy customizations
|
||||
current/ # current checked-in package directory
|
||||
```
|
||||
|
||||
Patch-authoring flow:
|
||||
|
||||
```bash
|
||||
# 1. Make the intended change in pkgbuilds/package-name/
|
||||
|
||||
# 2. Recreate the scratch workspace
|
||||
bin/package-worktree package-name --dir /tmp/package-name-worktree
|
||||
|
||||
# 3. Inspect drift from patched -> current
|
||||
# For multi-file changes, inspect this and split into focused patches.
|
||||
diff -ruN /tmp/package-name-worktree/patched /tmp/package-name-worktree/current
|
||||
|
||||
# For a single PKGBUILD change, write a patch like this:
|
||||
mkdir -p pkgbuilds/package-name/.omarchy/patches
|
||||
(
|
||||
cd /tmp/package-name-worktree/patched
|
||||
diff -u --label a/PKGBUILD --label b/PKGBUILD \
|
||||
PKGBUILD /tmp/package-name-worktree/current/PKGBUILD || true
|
||||
) > pkgbuilds/package-name/.omarchy/patches/my-fix.patch
|
||||
|
||||
# 4. Verify the package is reproducible from AUR + .omarchy
|
||||
bin/sync-aur package-name
|
||||
bin/package-worktree package-name --dir /tmp/package-name-check
|
||||
diff -ruN /tmp/package-name-check/patched /tmp/package-name-check/current
|
||||
```
|
||||
|
||||
For dynamic changes that depend on the current upstream version, add `pkgbuilds/package-name/.omarchy/post-sync.sh`. The hook runs after the AUR package is copied into a temporary worktree and before the Omarchy pkgrel suffix is applied. After patches/hooks/metadata pkgrel overrides, `bin/sync-aur` removes AUR-only `.SRCINFO` and `.gitignore` files before writing the package back.
|
||||
|
||||
### Custom Package
|
||||
|
||||
```bash
|
||||
bin/add-package my-package --local --scaffold
|
||||
# Fill in PKGBUILD and package files
|
||||
bin/add-package my-package --scaffold
|
||||
# Fill in PKGBUILD, package files, and upstream metadata
|
||||
bin/repo release --package my-package
|
||||
```
|
||||
|
||||
@@ -691,10 +795,15 @@ bin/repo release --package my-package
|
||||
- Mirrors: mirror.omarchy.org, rackspace, pkgbuild.com
|
||||
|
||||
### aarch64
|
||||
- QEMU emulation required on x86_64 hosts (slower)
|
||||
- Uses Arch Linux ARM repositories
|
||||
- Built on the repository host like x86_64; under QEMU when the host is x86_64
|
||||
- On an ARM host, package builds and the signing/database utility containers
|
||||
run natively; only an explicitly requested x86_64 package build is emulated
|
||||
- Uses Arch Linux ARM repositories through the same HTTPS mirror for every
|
||||
channel (Arch Linux ARM publishes no dated snapshots to pin a channel's base)
|
||||
- Additional repos: `[alarm]`, `[aur]`
|
||||
- Same workflow, just add `--arch aarch64`
|
||||
- Same workflow, just add `--arch aarch64`; the scheduled pipeline runs it
|
||||
automatically once `aarch64` is in `PUBLISHED_ARCHES`
|
||||
- Packages whose `arch=()` lacks `aarch64` are skipped, not failed
|
||||
|
||||
### Building for Both Architectures
|
||||
|
||||
@@ -714,12 +823,73 @@ bin/repo sync --arch aarch64
|
||||
|
||||
The build system automatically handles inter-package dependencies:
|
||||
|
||||
1. Parses `depends=()` and `makedepends=()` from PKGBUILDs
|
||||
2. Builds in correct order
|
||||
3. Makes newly-built packages available via temporary `[omarchy-build]` repo
|
||||
1. Plans dependency order once, including `depends`, `makedepends`,
|
||||
`checkdepends`, and their architecture-specific arrays.
|
||||
2. Builds each package in a fresh container. Installed packages and changes
|
||||
to the container's system files cannot carry over to the next build.
|
||||
3. Shares successful artifacts through the temporary `[omarchy-build]` repo,
|
||||
installing newly built prerequisites in each consumer's container.
|
||||
4. Blocks consumers of a failed prerequisite while continuing independent
|
||||
builds. Any failure still prevents the release from publishing.
|
||||
|
||||
Example: If `aether` depends on `hyprshade`, `hyprshade` is built first.
|
||||
|
||||
Isolation also lets the release and dev Omarchy pairs build in the same run:
|
||||
Flea can install `omarchy` without preventing `omarchy-dev` from installing
|
||||
its conflicting settings package in a different container.
|
||||
|
||||
Pacman downloads are cached under `cache/pacman/<channel>/<arch>/` across
|
||||
containers and runs. The installed package database is never shared. Each
|
||||
container updates its base system before resolving build dependencies, so a
|
||||
cached builder image cannot cause a partial system upgrade. The existing
|
||||
`OMARCHY_KEEP_BUILD_WORKSPACE`, `OMARCHY_SKIP_BUILDER_IMAGE`, and
|
||||
`OMARCHY_DEFER_RUNTIME_DEPS` flags retain their behavior.
|
||||
|
||||
`tests/build-isolation.sh` exercises conflicting package pairs, failed
|
||||
prerequisites, resumed builds, cache replacement, and deferred dependencies
|
||||
using real containers and pacman transactions. It uses the prepared builder
|
||||
image, or an image named by `TEST_BUILDER_IMAGE`; CI builds the small fixture
|
||||
image in `tests/build-isolation.Dockerfile`.
|
||||
|
||||
### Daily builder images
|
||||
|
||||
`Refresh builder images` builds fresh `edge` environments daily at 04:23 UTC,
|
||||
when their inputs change on `master`, and on manual dispatch. x86_64 and
|
||||
aarch64 build on native GitHub-hosted runners, without occupying the DO
|
||||
package-builder pool. Each candidate must pass `tests/build-isolation.sh`,
|
||||
including real package builds, before publication to
|
||||
`ghcr.io/omacom/omarchy-pkg-builder`. Only `master` in this repository can
|
||||
publish; PR workflows cannot replace the shared images.
|
||||
PRs that change image inputs also build and test both candidates on native
|
||||
runners, with a read-only token and no registry publication.
|
||||
|
||||
The compatibility tag contains the architecture, mirror, and a hash of the
|
||||
entire `build/` context, including executable bits and symlink targets but
|
||||
excluding checkout timestamps and ownership. This deliberately invalidates
|
||||
images when mounted build scripts change too. `v1` identifies the image build
|
||||
contract; change it if the invocation or compatibility rules change. Each
|
||||
successful refresh also gets a run-specific tag for diagnosis and rollback.
|
||||
A failed build, isolation test, or push leaves the previous compatible image
|
||||
selected. Scheduled builds use `--pull --no-cache` so unchanged Dockerfiles
|
||||
still pick up fresh Arch packages.
|
||||
|
||||
To build and test a candidate locally:
|
||||
|
||||
```bash
|
||||
bin/builder-image key --arch x86_64 --mirror edge
|
||||
bin/builder-image build --arch x86_64 --mirror edge --tag builder-candidate:test --fresh
|
||||
CONTAINER_ENGINE=docker TEST_BUILDER_IMAGE=builder-candidate:test tests/build-isolation.sh
|
||||
```
|
||||
|
||||
The workflow uses its repository `GITHUB_TOKEN` with `packages: write`; no
|
||||
registry PAT is needed. **First publication needs one package setting:** GHCR
|
||||
creates the package private. In the `omacom/omarchy-pkg-builder` package
|
||||
settings, change visibility to **Public**, then rerun the failed refresh job.
|
||||
The workflow checks anonymous registry access before advancing the compatible
|
||||
tag, so fork PRs will not be directed to an image they cannot pull. Subsequent
|
||||
refreshes preserve that package visibility. This change only produces images;
|
||||
package jobs keep their existing behavior until image consumption is enabled.
|
||||
|
||||
## Version Management
|
||||
|
||||
Packages are only rebuilt if:
|
||||
@@ -736,17 +906,70 @@ The repository includes GitHub workflows and systemd services for automated rele
|
||||
|
||||
#### GitHub Workflows
|
||||
|
||||
1. **sync-aur.yml** (Every 6 hours): Syncs AUR packages according to `.omarchy/package.json` and opens a PR when changes are found.
|
||||
2. **sync-upstream.yml** (Every 6 hours): Runs `.omarchy/upstream.sh` for packages that track a vendor release feed and opens a PR when a newer version is out.
|
||||
3. **sync-rebuilds.yml** (Every 6 hours): Bumps pkgrel for packages whose `rebuild_on` dependencies have moved in the official repositories and opens a PR.
|
||||
1. **sync-upstream.yml** (Every 6 hours): Watches direct upstream feeds and updates owned recipes on the reviewed lane. Successful package updates reach a PR even if another feed fails; failed recipes stay untouched and the workflow remains red.
|
||||
2. **sync-rebuilds.yml** (Every 6 hours): Bumps pkgrel for packages whose `rebuild_on` dependencies have moved in the official repositories, opens a PR, and enables auto-merge. The PR lands once its build checks pass; a rebuild that fails stays an open red PR for a maintainer.
|
||||
3. **track-branches.yml** (Every 2 hours): The unattended lane. Pins every `"auto_merge": true` package to the tip of its watched branch once its commit timestamp clears `min_release_age`, opens one PR for all of them, and enables auto-merge. Packages pinned from the same branch move together or not at all, including targeted syncs. The PR builds like any other; a tip that fails to build stays an open red PR until the next tick supersedes it.
|
||||
|
||||
The tracking and rebuild PRs and their auto-merge use the PAT stored in `PKGS_BOT_TOKEN`, with
|
||||
Contents and Pull requests write access to this repository and an owner trusted
|
||||
to trigger builds. The existing controller PAT can be reused. No GitHub App is
|
||||
required. The built-in Actions `GITHUB_TOKEN` cannot drive the unattended
|
||||
build-and-publish chain, so both workflows require this secret before they run.
|
||||
The reviewed upstream sync continues to use `GITHUB_TOKEN` and requires
|
||||
maintainer approval as before. See [setup instructions](docs/upstream-sources.md#enable-unattended-branch-updates).
|
||||
|
||||
Scheduled runs regenerate one shared PR (`auto/sync-upstream`, `auto/sync-rebuilds`)
|
||||
from master. A manual run with the `packages` input only regenerates those
|
||||
packages, so it opens its own PR on `auto/sync-upstream-<packages>` (or
|
||||
`auto/sync-rebuilds-<packages>`) rather than replacing the shared PR's other
|
||||
pending updates. The next scheduled run still picks the same update up in the
|
||||
shared PR if it has not merged by then; identical package trees reuse the same
|
||||
build artifacts.
|
||||
|
||||
Upstream sync PRs are pushed with `GITHUB_TOKEN`, so GitHub holds their build and test
|
||||
runs for approval on every push and starts no `pull_request_target` workflow
|
||||
for them. The upstream sync labels its own PRs **`build-approved`**, and
|
||||
its `approve` job releases the held runs for each commit they push, including
|
||||
the push that opens the PR. A push to an `auto/sync-*` branch does not cancel
|
||||
the PR's in-flight build: the new build waits for it and then reuses its
|
||||
artifacts, so a long aarch64 build is not restarted by every sync.
|
||||
|
||||
Package PRs that are trusted to build also merge themselves.
|
||||
`auto-merge-pr.yml` enables auto-merge (with `PKGS_BOT_TOKEN`, so the merge
|
||||
publishes) on any open, non-draft PR whose author is a collaborator, vouched,
|
||||
or a bot, or that carries **`build-approved`**, and that changes only
|
||||
packages: anything under `pkgbuilds/`, plus the test a package PR brings with
|
||||
it (a new file under `tests/`, and `test.yml` lines that only run new
|
||||
`./tests/*.sh`). The PR lands once `result`, `self-tests` and
|
||||
`build-isolation` pass; a red build stays open. PRs that also touch
|
||||
workflows, scripts, build tooling or existing tests still need a maintainer, as does
|
||||
the upstream sync (`auto/sync-upstream`), which labels itself. Removing
|
||||
`build-approved` withdraws the auto-merge it armed. For a PR opened before
|
||||
the workflow existed, run it by hand: `gh workflow run auto-merge-pr.yml -f pr=<number>`.
|
||||
|
||||
To approve builds for an unvouched contributor's PR, apply **`build-approved`**.
|
||||
Until approval, the PR shows **Awaiting build approval** and its required
|
||||
`result` check stays pending, keeping the PR blocked from merging without
|
||||
reporting a failed build. Actual build failures and denouncements still fail.
|
||||
Applying the label triggers a package build and automatically releases GitHub's
|
||||
pending build and test workflows for that PR's current commit. The approval workflow
|
||||
runs only trusted default-branch code; package builds and tests stay in the
|
||||
ordinary PR workflows. It may take a few minutes for GitHub to register and
|
||||
release all the runs.
|
||||
|
||||
The label stays effective for that PR while attached, including later commits;
|
||||
it does not vouch for the author's other PRs. Removing it stops further label
|
||||
approvals, but does not cancel runs already released. An explicit denouncement
|
||||
in `.github/VOUCHED.td` still blocks builds. If the approval workflow times out,
|
||||
remove and reapply the label to retry.
|
||||
|
||||
#### Systemd Services
|
||||
|
||||
All four units run **every 5 minutes**, staggered by a minute each, so a push
|
||||
reaches the mirror in minutes rather than hours:
|
||||
|
||||
1. **check-versions** (`*:0/5`): Pulls latest from git, compares PKGBUILD versions to published versions, creates state files if builds are needed
|
||||
2. **auto-release-edge** (`*:1/5`): If a state file exists, builds all edge packages that need updates
|
||||
1. **check-versions** (`*:0/5`): Pulls latest from git, compares PKGBUILD versions to published versions for every published architecture, creates one state file per channel and architecture if builds are needed
|
||||
2. **auto-release-edge** (`*:1/5`): For each published architecture with a state file, builds all edge packages that need updates
|
||||
3. **auto-release-rc** (`*:2/5`): Builds fast-ring packages for rc, from the main checkout like the other two — natively in the rc image, not copied from another channel. The pinned release pair is built separately by `omarchy-release rc` in the `rc` branch worktree
|
||||
4. **auto-release-stable** (`*:3/5`): If a state file exists, builds `release_ring=fast` packages for stable and replicates them to rc
|
||||
|
||||
@@ -760,10 +983,11 @@ That cadence is only safe because of three guards:
|
||||
an operator expects. `check-versions` takes it too — its `git pull` would
|
||||
otherwise swap PKGBUILDs out from under a running build.
|
||||
- **Backoff on failure.** A failed release records the attempt in
|
||||
`.build-failed-<channel>` and backs off exponentially — 10m, 20m, 40m, up to
|
||||
`.build-failed-<channel>-<arch>` and backs off exponentially — 10m, 20m, 40m, up to
|
||||
a 6h ceiling — instead of rebuilding the same broken tree every 5 minutes.
|
||||
**Any new commit clears the backoff immediately**, since a push is the most
|
||||
likely fix. Clear it by hand with `rm /root/.state/.build-failed-<channel>`.
|
||||
likely fix. Clear it by hand with
|
||||
`rm /root/.state/.build-failed-<channel>-<arch>`.
|
||||
- **Quiet when idle.** With nothing queued a tick exits without output, so the
|
||||
journal shows the runs that mattered rather than 288 no-ops a day.
|
||||
|
||||
@@ -816,10 +1040,14 @@ bin/repo timers --local # inspect this machine instead
|
||||
```
|
||||
|
||||
State files are stored in `/root/.state/`:
|
||||
- `.sync-needed-<channel>` — the packages queued for that channel, one per
|
||||
line; the release run reads them to name what it is building
|
||||
- `.build-failed-<channel>` — consecutive failure count, timestamp, and the
|
||||
commit it failed on (drives the backoff; removing it forces a retry)
|
||||
- `.sync-needed-<channel>-<arch>` — the packages queued for that channel and
|
||||
architecture, one per line; the release run reads them to name what it is
|
||||
building
|
||||
- `.build-failed-<channel>-<arch>` — consecutive failure count, timestamp, and
|
||||
the commit it failed on (drives the backoff; removing it forces a retry)
|
||||
|
||||
Legacy files without the architecture suffix are consumed once as x86_64
|
||||
state, so upgrading the host does not lose an in-flight build.
|
||||
|
||||
### Schedule (America/New_York)
|
||||
|
||||
|
||||
+19
-20
@@ -6,7 +6,7 @@ source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
|
||||
PACKAGE=""
|
||||
SOURCE="aur"
|
||||
SOURCE="local"
|
||||
SYNC="true"
|
||||
RELEASE_RING=""
|
||||
AUR_PACKAGE=""
|
||||
@@ -17,14 +17,14 @@ usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 <package> [OPTIONS]
|
||||
|
||||
Create pkgbuilds/<package> with Omarchy metadata. AUR packages are synced
|
||||
immediately after metadata is written.
|
||||
Create an Omarchy-owned package. --source aur imports a starting recipe once;
|
||||
future releases must use an upstream watch, provider, or hook.
|
||||
|
||||
Options:
|
||||
--source <aur|local> Package source (default: aur)
|
||||
--source <aur|local> Initial recipe source (default: local)
|
||||
--local Shortcut for --source local
|
||||
--aur <name> AUR package name when different from local directory
|
||||
--no-sync For AUR packages, mark sync disabled after initial setup
|
||||
--no-sync Keep the imported recipe manually maintained
|
||||
--fast Put package in the fast release ring
|
||||
--release-ring <ring> Release ring (currently: fast)
|
||||
--scaffold For local packages, create a starter PKGBUILD
|
||||
@@ -32,9 +32,9 @@ Options:
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$0 yay
|
||||
$0 spotify --fast
|
||||
$0 signal-desktop --no-sync
|
||||
$0 yay --source aur
|
||||
$0 spotify --source aur --fast
|
||||
$0 signal-desktop --source aur --no-sync
|
||||
$0 omarchy-zsh --local --scaffold
|
||||
EOF
|
||||
}
|
||||
@@ -97,6 +97,10 @@ if [[ -z "$PACKAGE" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! $PACKAGE =~ ^[a-zA-Z0-9@_+][a-zA-Z0-9@._+-]*$ ]]; then
|
||||
print_error "Invalid package name: $PACKAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$SOURCE" in
|
||||
aur|local) ;;
|
||||
@@ -112,6 +116,11 @@ PACKAGE_DIR="$PKGBUILDS_DIR/$PACKAGE"
|
||||
OMARCHY_DIR="$PACKAGE_DIR/.omarchy"
|
||||
METADATA_FILE="$OMARCHY_DIR/package.json"
|
||||
|
||||
if [[ "$SOURCE" == aur && -f "$PACKAGE_DIR/PKGBUILD" ]]; then
|
||||
print_error "Refusing to replace the maintained recipe for $PACKAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$METADATA_FILE" && "$FORCE" != true ]]; then
|
||||
print_error "Package metadata already exists: $METADATA_FILE"
|
||||
print_info "Use --force to overwrite it"
|
||||
@@ -146,18 +155,8 @@ jq -n "${jq_args[@]}" "$jq_filter" > "$METADATA_FILE"
|
||||
print_success "Wrote $METADATA_FILE"
|
||||
|
||||
if [[ "$SOURCE" == "aur" ]]; then
|
||||
if [[ "$SYNC" == "false" ]]; then
|
||||
# Temporarily sync once, then restore sync=false so future automated syncs skip it.
|
||||
tmpfile=$(mktemp)
|
||||
jq 'del(.sync)' "$METADATA_FILE" > "$tmpfile"
|
||||
mv "$tmpfile" "$METADATA_FILE"
|
||||
"$BUILD_ROOT/bin/sync-aur" "$PACKAGE"
|
||||
jq '. + {sync: false}' "$METADATA_FILE" > "$tmpfile"
|
||||
mv "$tmpfile" "$METADATA_FILE"
|
||||
print_info "AUR sync disabled for future runs"
|
||||
else
|
||||
"$BUILD_ROOT/bin/sync-aur" "$PACKAGE"
|
||||
fi
|
||||
"$BUILD_ROOT/bin/import-aur" "$PACKAGE"
|
||||
|
||||
else
|
||||
if [[ "$SCAFFOLD" == true && ! -f "$PACKAGE_DIR/PKGBUILD" ]]; then
|
||||
cat > "$PACKAGE_DIR/PKGBUILD" <<EOF
|
||||
|
||||
+57
-10
@@ -25,6 +25,22 @@ SKIP_PROD_CHECK=false
|
||||
FAST_RING_ONLY=false
|
||||
BOOTSTRAP=false
|
||||
PACKAGES=""
|
||||
ALL_ARCHES=false
|
||||
|
||||
# Kept for --arch all, which re-invokes this script per architecture with the
|
||||
# other arguments unchanged (minus the --arch all pair itself).
|
||||
ORIGINAL_ARGS=()
|
||||
arch_option=false
|
||||
for arg in "$@"; do
|
||||
if [[ "$arch_option" == true ]]; then
|
||||
[[ "$arg" != "all" ]] && ORIGINAL_ARGS+=("--arch" "$arg")
|
||||
arch_option=false
|
||||
elif [[ "$arg" == "--arch" ]]; then
|
||||
arch_option=true
|
||||
else
|
||||
ORIGINAL_ARGS+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 --from <channel> --to <channel> [OPTIONS]"
|
||||
@@ -36,7 +52,8 @@ usage() {
|
||||
echo " or --bootstrap (one-time initial seed)"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --arch <arch> Target architecture (x86_64 or aarch64, default: x86_64)"
|
||||
echo " --arch <arch>|all Target architecture (x86_64 or aarch64, default: x86_64);"
|
||||
echo " all = every published architecture, one after another"
|
||||
echo " --package <names...> Advance only the named package(s)"
|
||||
echo " --fast-ring Restrict to fast-ring packages (stable -> rc parity copy)"
|
||||
echo " --bootstrap One-time stable -> rc seed before forward-only enforcement"
|
||||
@@ -65,8 +82,14 @@ while [[ $# -gt 0 ]]; do
|
||||
shift 2
|
||||
;;
|
||||
--arch)
|
||||
ARCH="$2"
|
||||
update_arch_paths
|
||||
if [[ "$2" == "all" ]]; then
|
||||
ALL_ARCHES=true
|
||||
ARCH=all
|
||||
else
|
||||
require_valid_arch "$2"
|
||||
ARCH="$2"
|
||||
update_arch_paths
|
||||
fi
|
||||
shift 2
|
||||
;;
|
||||
--package)
|
||||
@@ -134,6 +157,31 @@ case "$FROM->$TO" in
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$ALL_ARCHES" == true ]]; then
|
||||
# Hold one lock across the whole operation so a timer cannot mutate a
|
||||
# channel between architectures. Check every source database before moving
|
||||
# the first one; a failed sync can still require an idempotent retry, but a
|
||||
# missing architecture never creates a knowingly partial advance.
|
||||
if [[ "$DRY_RUN" != true ]]; then
|
||||
acquire_release_lock || exit 1
|
||||
fi
|
||||
|
||||
ARCHES=$(published_arches)
|
||||
for arch in $ARCHES; do
|
||||
source_db="$REPO_ROOT/$FROM/$arch/omarchy.db.tar.zst"
|
||||
if [[ ! -f "$source_db" ]]; then
|
||||
print_error "Source database not found: $source_db"
|
||||
echo "Nothing has been published to the $FROM channel for $arch."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
for arch in $ARCHES; do
|
||||
"$0" --arch "$arch" "${ORIGINAL_ARGS[@]}" || exit $?
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SOURCE_DIR="$REPO_ROOT/$FROM/$ARCH"
|
||||
TARGET_DIR="$REPO_ROOT/$TO/$ARCH"
|
||||
SOURCE_DB="$SOURCE_DIR/omarchy.db.tar.zst"
|
||||
@@ -278,7 +326,7 @@ while IFS=$'\t' read -r name base filename; do
|
||||
done < <(read_manifest)
|
||||
|
||||
echo ""
|
||||
print_info "Copied: $COPIED | Already present: $PRESENT | Not eligible: $SKIPPED"
|
||||
print_info "Copied: $COPIED | Already present: $PRESENT | Differing (kept): ${#DIFFERING[@]} | Not eligible: $SKIPPED"
|
||||
|
||||
if [[ ${#MISSING_FILES[@]} -gt 0 ]]; then
|
||||
print_error "${#MISSING_FILES[@]} package(s) named in the $FROM database are missing on disk:"
|
||||
@@ -297,13 +345,12 @@ if [[ ${#MISSING_SIGS[@]} -gt 0 ]]; then
|
||||
fi
|
||||
|
||||
if [[ ${#DIFFERING[@]} -gt 0 ]]; then
|
||||
print_error "${#DIFFERING[@]} file(s) already published in $TO with DIFFERENT bytes:"
|
||||
print_warning "${#DIFFERING[@]} file(s) already published in $TO with different bytes — kept as-is:"
|
||||
printf ' %s\n' "${DIFFERING[@]}"
|
||||
echo "Published filenames are never rewritten, and two artifacts fighting over"
|
||||
echo "one name means something built twice from different inputs. Resolve it"
|
||||
echo "deliberately: bump pkgrel and rebuild so the new artifact gets a new"
|
||||
echo "filename, or remove the source copy if the destination is correct."
|
||||
exit 1
|
||||
echo "Published filenames are never rewritten, so the $TO copy stays authoritative."
|
||||
echo "A same-name collision means this version reached $TO through another lineage"
|
||||
echo "(native build, bootstrap seed, or a same-version rebuild) and the package has"
|
||||
echo "not moved in $FROM since; it advances under a new filename when it does."
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
|
||||
+104
-57
@@ -1,6 +1,10 @@
|
||||
#!/bin/bash
|
||||
# Run the release workflow for a channel when work is queued.
|
||||
# Usage: auto-release <edge|rc|stable>
|
||||
# Usage: auto-release <edge|rc|stable> [<arch>|all]
|
||||
#
|
||||
# With no architecture (what the timers pass), every published architecture
|
||||
# is processed in turn, each against its own queue and its own backoff, so a
|
||||
# failing aarch64 build never holds up x86_64 or the other way round.
|
||||
#
|
||||
# Safe to run on a tight schedule. Three guards make that true:
|
||||
#
|
||||
@@ -21,7 +25,7 @@ source "$BUILD_ROOT/helpers/paths.sh"
|
||||
source "$BUILD_ROOT/helpers/lock-helpers.sh"
|
||||
|
||||
MIRROR="${1:-}"
|
||||
STATE_DIR="${OMARCHY_STATE_DIR:-/root/.state}"
|
||||
ARCH_ARG="${2:-all}"
|
||||
|
||||
# Backoff schedule: 10m, 20m, 40m, 80m, 160m, 320m, then hourly-ish forever
|
||||
# (capped at 6h, the cadence this system ran at before frequent timers).
|
||||
@@ -29,8 +33,9 @@ BACKOFF_BASE_SECONDS="${OMARCHY_BACKOFF_BASE:-600}"
|
||||
BACKOFF_MAX_SECONDS="${OMARCHY_BACKOFF_MAX:-21600}"
|
||||
|
||||
if [[ -z "$MIRROR" ]]; then
|
||||
print_error "Usage: $0 <mirror>"
|
||||
print_error "Usage: $0 <mirror> [<arch>|all]"
|
||||
echo " mirror: edge, rc, or stable"
|
||||
echo " arch: one of $VALID_ARCHES, or all (default) for every published architecture"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -39,17 +44,13 @@ if [[ "$MIRROR" != "edge" && "$MIRROR" != "rc" && "$MIRROR" != "stable" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STATE_FILE="$STATE_DIR/.sync-needed-$MIRROR"
|
||||
FAIL_FILE="$STATE_DIR/.build-failed-$MIRROR"
|
||||
|
||||
# Nothing queued: stay quiet. At a 5-minute cadence this is most invocations,
|
||||
# and a header for each would bury the runs that matter in the journal.
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
exit 0
|
||||
if [[ "$ARCH_ARG" == "all" ]]; then
|
||||
ARCHES=$(published_arches)
|
||||
else
|
||||
require_valid_arch "$ARCH_ARG"
|
||||
ARCHES="$ARCH_ARG"
|
||||
fi
|
||||
|
||||
print_header "Processing Sync for $MIRROR"
|
||||
|
||||
# The build inputs are this checkout's contents; its HEAD identifies them.
|
||||
current_fingerprint() {
|
||||
git -C "$BUILD_ROOT" rev-parse HEAD 2>/dev/null || echo "unknown"
|
||||
@@ -65,57 +66,103 @@ backoff_seconds() {
|
||||
echo "$delay"
|
||||
}
|
||||
|
||||
FAIL_COUNT=0
|
||||
if [[ -f "$FAIL_FILE" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$FAIL_FILE" 2>/dev/null || true
|
||||
FAIL_COUNT="${FAILURE_COUNT:-0}"
|
||||
failed_at="${FAILURE_AT:-0}"
|
||||
failed_fingerprint="${FAILURE_FINGERPRINT:-}"
|
||||
# Returns 0 when this architecture's queue was processed (or was empty), 1 when
|
||||
# the release failed. Backoff and "someone else holds the lock" are not
|
||||
# failures: they are this tick deciding to do nothing.
|
||||
release_arch() {
|
||||
local arch="$1"
|
||||
local state_file fail_file
|
||||
state_file=$(sync_queue_file "$MIRROR" "$arch")
|
||||
fail_file=$(sync_fail_file "$MIRROR" "$arch")
|
||||
|
||||
if [[ "$failed_fingerprint" != "$(current_fingerprint)" ]]; then
|
||||
print_info "Repository changed since the last failure — clearing backoff and retrying"
|
||||
rm -f "$FAIL_FILE"
|
||||
FAIL_COUNT=0
|
||||
else
|
||||
delay=$(backoff_seconds "$FAIL_COUNT")
|
||||
now=$(date +%s)
|
||||
retry_at=$((failed_at + delay))
|
||||
if ((now < retry_at)); then
|
||||
print_warning "$MIRROR has failed $FAIL_COUNT time(s) on this tree — not retrying until $(date -d "@$retry_at" '+%H:%M:%S' 2>/dev/null || echo "+$((retry_at - now))s")"
|
||||
echo " Push a fix (any new commit clears this), or: rm $FAIL_FILE"
|
||||
exit 0
|
||||
fi
|
||||
print_info "Backoff elapsed — retrying $MIRROR (failure #$((FAIL_COUNT + 1)) if this fails)"
|
||||
# A queue written under the pre-architecture name belongs to x86_64.
|
||||
if [[ "$arch" == "x86_64" && ! -f "$state_file" && -f "$(legacy_sync_queue_file "$MIRROR")" ]]; then
|
||||
state_file=$(legacy_sync_queue_file "$MIRROR")
|
||||
fi
|
||||
if [[ "$arch" == "x86_64" && ! -f "$fail_file" && -f "$(legacy_sync_fail_file "$MIRROR")" ]]; then
|
||||
fail_file=$(legacy_sync_fail_file "$MIRROR")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Non-blocking: a build in progress means this tick has nothing to do.
|
||||
if ! try_release_lock; then
|
||||
holder=$(release_lock_holder)
|
||||
print_info "A release is already running (${holder:-holder unknown}) — skipping this tick"
|
||||
exit 0
|
||||
fi
|
||||
# Nothing queued: stay quiet. At a 5-minute cadence this is most
|
||||
# invocations, and a header for each would bury the runs that matter in
|
||||
# the journal.
|
||||
[[ -f "$state_file" ]] || return 0
|
||||
|
||||
print_info "State file found: $STATE_FILE"
|
||||
print_info "Starting release workflow for $MIRROR..."
|
||||
print_header "Processing Sync for $MIRROR ($arch)"
|
||||
|
||||
if "$BUILD_ROOT/bin/repo" release --mirror "$MIRROR" --skip-prod-check; then
|
||||
print_success "Release completed successfully for $MIRROR"
|
||||
rm -f "$STATE_FILE"
|
||||
rm -f "$FAIL_FILE"
|
||||
print_success "State file removed: $STATE_FILE"
|
||||
else
|
||||
status=$?
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
cat >"$FAIL_FILE" <<EOF
|
||||
FAILURE_COUNT=$FAIL_COUNT
|
||||
local fail_count=0 failed_at failed_fingerprint delay now retry_at
|
||||
if [[ -f "$fail_file" ]]; then
|
||||
FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT=""
|
||||
# shellcheck disable=SC1090
|
||||
source "$fail_file" 2>/dev/null || true
|
||||
fail_count="${FAILURE_COUNT:-0}"
|
||||
failed_at="${FAILURE_AT:-0}"
|
||||
failed_fingerprint="${FAILURE_FINGERPRINT:-}"
|
||||
|
||||
if [[ "$failed_fingerprint" != "$(current_fingerprint)" ]]; then
|
||||
print_info "Repository changed since the last failure — clearing backoff and retrying"
|
||||
rm -f "$fail_file"
|
||||
fail_count=0
|
||||
else
|
||||
delay=$(backoff_seconds "$fail_count")
|
||||
now=$(date +%s)
|
||||
retry_at=$((failed_at + delay))
|
||||
if ((now < retry_at)); then
|
||||
print_warning "$MIRROR ($arch) has failed $fail_count time(s) on this tree — not retrying until $(date -d "@$retry_at" '+%H:%M:%S' 2>/dev/null || echo "+$((retry_at - now))s")"
|
||||
echo " Push a fix (any new commit clears this), or: rm $fail_file"
|
||||
return 0
|
||||
fi
|
||||
print_info "Backoff elapsed — retrying $MIRROR ($arch) (failure #$((fail_count + 1)) if this fails)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Non-blocking: a build in progress means this tick has nothing to do. The
|
||||
# lock is reentrant, so once this run holds it the remaining architectures
|
||||
# run under the same acquisition.
|
||||
if ! try_release_lock; then
|
||||
local holder
|
||||
holder=$(release_lock_holder)
|
||||
print_info "A release is already running (${holder:-holder unknown}) — skipping this tick"
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_info "State file found: $state_file"
|
||||
print_info "Starting release workflow for $MIRROR ($arch)..."
|
||||
|
||||
if "$BUILD_ROOT/bin/repo" release --mirror "$MIRROR" --arch "$arch" --skip-prod-check; then
|
||||
print_success "Release completed successfully for $MIRROR ($arch)"
|
||||
local completed_state=("$state_file" "$fail_file")
|
||||
if [[ "$arch" == "x86_64" ]]; then
|
||||
completed_state+=("$(legacy_sync_queue_file "$MIRROR")" "$(legacy_sync_fail_file "$MIRROR")")
|
||||
fi
|
||||
if ! rm -f "${completed_state[@]}"; then
|
||||
print_error "Release succeeded, but its queue state could not be cleared"
|
||||
return 1
|
||||
fi
|
||||
print_success "State file removed: $state_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
fail_count=$((fail_count + 1))
|
||||
if ! cat >"$fail_file" <<EOF
|
||||
FAILURE_COUNT=$fail_count
|
||||
FAILURE_AT=$(date +%s)
|
||||
FAILURE_FINGERPRINT=$(current_fingerprint)
|
||||
EOF
|
||||
next=$(backoff_seconds "$FAIL_COUNT")
|
||||
print_error "Release failed for $MIRROR (attempt $FAIL_COUNT)"
|
||||
print_warning "State file retained for retry: $STATE_FILE"
|
||||
then
|
||||
print_error "Could not record failure state: $fail_file"
|
||||
return 1
|
||||
fi
|
||||
local next
|
||||
next=$(backoff_seconds "$fail_count")
|
||||
print_error "Release failed for $MIRROR ($arch) (attempt $fail_count)"
|
||||
print_warning "State file retained for retry: $state_file"
|
||||
print_warning "Backing off $((next / 60))m before the next attempt; a new commit retries sooner"
|
||||
exit "$status"
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
status=0
|
||||
for arch in $ARCHES; do
|
||||
release_arch "$arch" || status=1
|
||||
done
|
||||
exit "$status"
|
||||
@@ -13,6 +13,27 @@ print_header "Omarchy Package Builder"
|
||||
|
||||
DRY_RUN=false
|
||||
|
||||
# Knobs for builds driven from CI or resumed by hand. Each defaults to the
|
||||
# historical behaviour, so an unadorned `bin/build` is unchanged.
|
||||
#
|
||||
# OMARCHY_KEEP_BUILD_WORKSPACE=1 keep build-output/$MIRROR/$ARCH instead of
|
||||
# wiping it, so packages built by an earlier
|
||||
# job (or a previous, interrupted run) seed
|
||||
# the build database and resolve as
|
||||
# dependencies of what builds now.
|
||||
# OMARCHY_SKIP_BUILDER_IMAGE=1 use the omarchy-pkg-builder image already
|
||||
# present instead of building it; a workflow
|
||||
# that builds the image once with an external
|
||||
# BuildKit cache can then fan out over many
|
||||
# package jobs without each one rebuilding it.
|
||||
# OMARCHY_DEFER_RUNTIME_DEPS=true build the omarchy/omarchy-settings pair
|
||||
# with --nodeps (see build/build.sh); only
|
||||
# for a pipeline that verifies the install
|
||||
# transaction afterwards.
|
||||
KEEP_BUILD_WORKSPACE=${OMARCHY_KEEP_BUILD_WORKSPACE:-0}
|
||||
SKIP_BUILDER_IMAGE=${OMARCHY_SKIP_BUILDER_IMAGE:-0}
|
||||
DEFER_RUNTIME_DEPS=${OMARCHY_DEFER_RUNTIME_DEPS:-false}
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
@@ -65,7 +86,14 @@ while [[ $# -gt 0 ]]; do
|
||||
echo " $0 --arch aarch64"
|
||||
echo " $0 --mirror stable"
|
||||
echo " $0 --package yay"
|
||||
echo " $0 --package yay elephant cursor-bin"
|
||||
echo " $0 --package yay walker cursor-bin"
|
||||
echo ""
|
||||
echo "Environment (for CI and resumed builds; defaults keep today's behaviour):"
|
||||
echo " OMARCHY_KEEP_BUILD_WORKSPACE=1 keep build-output and reuse packages already there"
|
||||
echo " OMARCHY_SKIP_BUILDER_IMAGE=1 use the existing builder image instead of building it"
|
||||
echo " OMARCHY_DEFER_RUNTIME_DEPS=true build the omarchy pair with --nodeps (transaction verified later)"
|
||||
echo " OMARCHY_PUBLISHED_REPO_URL=<url> channel to plan and resolve against when no local tree exists"
|
||||
echo " (default https://pkgs.omarchy.org; empty disables the fallback)"
|
||||
echo ""
|
||||
exit 0
|
||||
;;
|
||||
@@ -76,18 +104,55 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
require_valid_arch "$ARCH"
|
||||
|
||||
if [[ $DEFER_RUNTIME_DEPS != "false" && $DEFER_RUNTIME_DEPS != "true" ]]; then
|
||||
print_error "OMARCHY_DEFER_RUNTIME_DEPS must be true or false"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Deferring runtime dependencies is only sound for the omarchy pair, and only
|
||||
# when both halves are built together: the pair depends on each other and on
|
||||
# packages that a sharded pipeline builds in other jobs, and the consumer of
|
||||
# this mode installs the assembled set in one verified transaction. Check the
|
||||
# request here so a misuse fails before Docker starts.
|
||||
if [[ $DEFER_RUNTIME_DEPS == "true" ]]; then
|
||||
deferred_runtime=0
|
||||
deferred_settings=0
|
||||
deferred_count=0
|
||||
for package in $PACKAGES; do
|
||||
((deferred_count += 1))
|
||||
case $package in
|
||||
omarchy|omarchy-dev) deferred_runtime=1 ;;
|
||||
omarchy-settings|omarchy-settings-dev) deferred_settings=1 ;;
|
||||
*)
|
||||
print_error "OMARCHY_DEFER_RUNTIME_DEPS only applies to the omarchy/omarchy-settings pair, not $package"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if (( deferred_runtime != 1 || deferred_settings != 1 || deferred_count != 2 )); then
|
||||
print_error "OMARCHY_DEFER_RUNTIME_DEPS requires --package with exactly the omarchy pair"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show target architecture and mirror after parsing args
|
||||
print_info "Target architecture: $ARCH"
|
||||
print_info "Mirror: $MIRROR"
|
||||
print_info "Build workspace: $BUILD_OUTPUT_DIR"
|
||||
print_info "Final output: $REPO_DIR"
|
||||
if [[ $DEFER_RUNTIME_DEPS == "true" ]]; then
|
||||
print_info "Runtime dependency checks: deferred to the install transaction"
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
print_warning "DRY RUN MODE - build plan only; no Docker or makepkg will run"
|
||||
print_warning "DRY RUN MODE - build plan only; no container or makepkg will run"
|
||||
ARCH="$ARCH" \
|
||||
MIRROR="$MIRROR" \
|
||||
PACKAGES="$PACKAGES" \
|
||||
DRY_RUN=true \
|
||||
DEFER_RUNTIME_DEPS="$DEFER_RUNTIME_DEPS" \
|
||||
PKGBUILDS_DIR="$PKGBUILDS_DIR" \
|
||||
BUILD_OUTPUT_DIR="$BUILD_OUTPUT_DIR" \
|
||||
FINAL_OUTPUT_DIR="$REPO_DIR" \
|
||||
@@ -100,21 +165,45 @@ fi
|
||||
# Create directories if they don't exist
|
||||
mkdir -p "$BUILD_OUTPUT_DIR" "$REPO_DIR" "$SRC_DIR"
|
||||
|
||||
# Check Docker is available
|
||||
check_docker
|
||||
# Check the selected container engine is available
|
||||
check_engine
|
||||
|
||||
# Setup QEMU for aarch64 builds on x86_64 hosts
|
||||
if [[ "$(uname -m)" == "x86_64" && "$ARCH" == "aarch64" ]]; then
|
||||
# Check if QEMU is already working
|
||||
if ! docker run --rm --platform linux/arm64 alpine:3.21 /bin/true >/dev/null 2>&1; then
|
||||
print_info "Setting up QEMU for ARM64 emulation..."
|
||||
setup_qemu
|
||||
# A foreign target architecture runs under QEMU user emulation. Probe by
|
||||
# actually running a container for the target platform: that is the only
|
||||
# test that covers both "binfmt not registered" and "registered but broken".
|
||||
HOST_ARCH=$(uname -m)
|
||||
[[ "$HOST_ARCH" == "arm64" ]] && HOST_ARCH=aarch64
|
||||
if [[ "$HOST_ARCH" != "$ARCH" ]]; then
|
||||
PROBE_IMAGE="alpine:3.21"
|
||||
[[ "$CONTAINER_ENGINE" == "podman" ]] && PROBE_IMAGE="docker.io/library/alpine:3.21"
|
||||
|
||||
# Rootless Podman cannot repair host binfmt state itself. Validate the flags
|
||||
# before the basic probe, because an F-only registration can start an ARM
|
||||
# container but silently breaks sudo inside it.
|
||||
if [[ "$CONTAINER_ENGINE" == "podman" ]]; then
|
||||
setup_qemu "$ARCH"
|
||||
fi
|
||||
|
||||
if ! "$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$ARCH")" "$PROBE_IMAGE" /bin/true >/dev/null 2>&1; then
|
||||
if [[ "$CONTAINER_ENGINE" == "podman" ]]; then
|
||||
print_error "QEMU $ARCH is registered, but the container probe failed"
|
||||
print_info "Refresh the registration with: sudo systemctl restart systemd-binfmt"
|
||||
exit 1
|
||||
else
|
||||
print_info "Setting up QEMU for $ARCH emulation on this $HOST_ARCH host..."
|
||||
setup_qemu "$ARCH"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean build-output directory to start fresh
|
||||
print_info "Cleaning build workspace..."
|
||||
rm -rf "$BUILD_OUTPUT_DIR"/*
|
||||
# Clean build-output directory to start fresh, unless the caller seeded it
|
||||
# with packages from an earlier job or is resuming an interrupted run.
|
||||
if [[ $KEEP_BUILD_WORKSPACE == "1" ]]; then
|
||||
print_info "Keeping existing build workspace..."
|
||||
else
|
||||
print_info "Cleaning build workspace..."
|
||||
rm -rf "${BUILD_OUTPUT_DIR:?}"/*
|
||||
fi
|
||||
mkdir -p "$BUILD_OUTPUT_DIR"
|
||||
|
||||
# Show package info
|
||||
@@ -124,18 +213,43 @@ else
|
||||
print_info "Building unscoped packages for $MIRROR mirror"
|
||||
fi
|
||||
|
||||
# Build/update the Docker image
|
||||
build_docker_image "$BUILD_DIR" "$ARCH" "$MIRROR"
|
||||
# Build/update the Docker image, unless the caller prepared the exact image
|
||||
# already (a workflow building it once with an external BuildKit cache). A
|
||||
# missing image is an error rather than a silent rebuild: the point of the
|
||||
# flag is that every job runs the same bytes.
|
||||
IMAGE_TAG="omarchy-pkg-builder:latest-$ARCH-$MIRROR"
|
||||
if [[ $SKIP_BUILDER_IMAGE == "1" ]]; then
|
||||
if ! "$CONTAINER_ENGINE" image inspect "$IMAGE_TAG" >/dev/null 2>&1; then
|
||||
print_error "Prepared builder image is unavailable: $IMAGE_TAG"
|
||||
exit 1
|
||||
fi
|
||||
print_info "Using prepared builder image: $IMAGE_TAG"
|
||||
else
|
||||
build_docker_image "$BUILD_DIR" "$ARCH" "$MIRROR"
|
||||
fi
|
||||
|
||||
print_info "Running package build..."
|
||||
print_info "Planning isolated package builds..."
|
||||
|
||||
# Create output directories if they don't exist
|
||||
mkdir -p "$BUILD_OUTPUT_DIR"
|
||||
mkdir -p "$REPO_DIR"
|
||||
|
||||
# Ensure output directories are writable by container user
|
||||
make_dir_writable "$BUILD_OUTPUT_DIR"
|
||||
make_dir_writable "$REPO_DIR"
|
||||
# Share downloaded archives, never /var/lib/pacman or an installed root.
|
||||
# Channel/architecture separation preserves each mirror's dependency set.
|
||||
PACKAGE_CACHE_DIR="$BUILD_ROOT/cache/pacman/$MIRROR/$ARCH"
|
||||
mkdir -p "$PACKAGE_CACHE_DIR"
|
||||
PLAN_DIR=$(mktemp -d "$SRC_DIR/build-plan.XXXXXX")
|
||||
trap 'rm -rf "$PLAN_DIR"' EXIT
|
||||
# Keep manifest directories host-owned so cleanup also works when Docker's
|
||||
# builder uid differs from the caller (as on GitHub runners).
|
||||
mkdir -p "$PLAN_DIR/artifacts"
|
||||
|
||||
# Rootful Docker writes as the image uid, so retain its existing permission
|
||||
# workaround. Rootless Podman uses keep-id and must leave ownership/modes alone.
|
||||
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||
make_dir_writable "$BUILD_OUTPUT_DIR"
|
||||
make_dir_writable "$PLAN_DIR"
|
||||
fi
|
||||
|
||||
# Build Docker arguments
|
||||
DOCKER_ARGS=(
|
||||
@@ -144,26 +258,101 @@ DOCKER_ARGS=(
|
||||
-e MIRROR="$MIRROR"
|
||||
-e PACKAGES="$PACKAGES"
|
||||
-e OMARCHY_RC_PINS="${OMARCHY_RC_PINS:-}"
|
||||
-e OMARCHY_PUBLISHED_REPO_URL="${OMARCHY_PUBLISHED_REPO_URL-https://pkgs.omarchy.org}"
|
||||
-e DEFER_RUNTIME_DEPS="$DEFER_RUNTIME_DEPS"
|
||||
-e BUILD_PLAN_DIR=/build-plan
|
||||
-v "$PLAN_DIR:/build-plan"
|
||||
-v "$PACKAGE_CACHE_DIR:/var/cache/pacman/pkg"
|
||||
-v "$BUILD_ROOT/build-output:/build-output"
|
||||
-v "$REPO_ROOT:/pkgs.omarchy.org"
|
||||
-v "$REPO_ROOT:/pkgs.omarchy.org:ro"
|
||||
-v "$BUILD_DIR:/build:ro"
|
||||
-v "$BUILD_ROOT/helpers:/helpers:ro"
|
||||
-v "$BUILD_ROOT/pkgbuilds:/pkgbuilds:ro"
|
||||
)
|
||||
|
||||
# Run the builder with assembled args
|
||||
IMAGE_TAG="omarchy-pkg-builder:latest-$ARCH-$MIRROR"
|
||||
# Podman-created images can leave WORKDIR owned by a remapped uid. Mount the
|
||||
# existing host-user-owned workspace so the builder can write there.
|
||||
if [[ "$CONTAINER_ENGINE" == "podman" ]]; then
|
||||
DOCKER_ARGS+=(-v "$SRC_DIR:/src")
|
||||
fi
|
||||
|
||||
# Plan once against the published database, then keep that order throughout
|
||||
# the run. Each package sees the staged artifacts but starts with a fresh
|
||||
# pacman database and root filesystem, even after a failed build.
|
||||
PLATFORM_ARG=$(get_platform_arg "$ARCH")
|
||||
|
||||
docker run $PLATFORM_ARG "${DOCKER_ARGS[@]}" "$IMAGE_TAG" /build/build.sh
|
||||
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" "$PLATFORM_ARG" "${DOCKER_ARGS[@]}" \
|
||||
-e DRY_RUN=true "$IMAGE_TAG" /build/build.sh
|
||||
|
||||
BUILD_RESULT=$?
|
||||
mapfile -t ORDERED_PACKAGES < "$PLAN_DIR/packages"
|
||||
mapfile -t SKIPPED_PACKAGES < "$PLAN_DIR/skipped"
|
||||
SUCCESSFUL_PACKAGES=()
|
||||
FAILED_PACKAGES=()
|
||||
BLOCKED_PACKAGES=()
|
||||
declare -A BUILD_STATUS=()
|
||||
|
||||
for package in "${ORDERED_PACKAGES[@]}"; do
|
||||
blocked_by=""
|
||||
while read -r consumer dependency; do
|
||||
if [[ "$consumer" == "$package" && "${BUILD_STATUS[$dependency]:-}" != success ]]; then
|
||||
blocked_by="$dependency"
|
||||
break
|
||||
fi
|
||||
done < "$PLAN_DIR/dependencies"
|
||||
|
||||
if [[ -n "$blocked_by" ]]; then
|
||||
print_warning "$package blocked by unsuccessful dependency: $blocked_by"
|
||||
BUILD_STATUS[$package]=blocked
|
||||
BLOCKED_PACKAGES+=("$package")
|
||||
continue
|
||||
fi
|
||||
|
||||
print_info "Building $package in a fresh container..."
|
||||
if "$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" "$PLATFORM_ARG" "${DOCKER_ARGS[@]}" \
|
||||
-e BUILD_PACKAGE="$package" "$IMAGE_TAG" /build/build.sh; then
|
||||
BUILD_STATUS[$package]=success
|
||||
SUCCESSFUL_PACKAGES+=("$package")
|
||||
else
|
||||
BUILD_STATUS[$package]=failed
|
||||
FAILED_PACKAGES+=("$package")
|
||||
fi
|
||||
done
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
if [[ $BUILD_RESULT -eq 0 ]]; then
|
||||
print_success "Build completed successfully!"
|
||||
else
|
||||
print_warning "Some packages failed (see details above)"
|
||||
exit $BUILD_RESULT
|
||||
print_header "Build Summary"
|
||||
echo " Total packages: ${#ORDERED_PACKAGES[@]}"
|
||||
echo " Built: ${#SUCCESSFUL_PACKAGES[@]}"
|
||||
echo " Skipped: ${#SKIPPED_PACKAGES[@]} (up-to-date or excluded)"
|
||||
echo " Failed: ${#FAILED_PACKAGES[@]}"
|
||||
echo " Blocked: ${#BLOCKED_PACKAGES[@]}"
|
||||
|
||||
# The release caller supplies a fresh directory. A completed result
|
||||
# distinguishes package failures from an interrupted/failed orchestrator;
|
||||
# only artifacts belonging to fully successful builds may be published.
|
||||
if [[ -n "${OMARCHY_BUILD_RESULT_DIR:-}" ]]; then
|
||||
mkdir -p "$OMARCHY_BUILD_RESULT_DIR"
|
||||
: > "$OMARCHY_BUILD_RESULT_DIR/artifacts"
|
||||
for package in "${SUCCESSFUL_PACKAGES[@]}"; do
|
||||
cat "$PLAN_DIR/artifacts/$package" >> "$OMARCHY_BUILD_RESULT_DIR/artifacts"
|
||||
done
|
||||
printf '%s\n' "${FAILED_PACKAGES[@]}" | sed '/^$/d' > "$OMARCHY_BUILD_RESULT_DIR/failed"
|
||||
printf '%s\n' "${BLOCKED_PACKAGES[@]}" | sed '/^$/d' > "$OMARCHY_BUILD_RESULT_DIR/blocked"
|
||||
touch "$OMARCHY_BUILD_RESULT_DIR/complete"
|
||||
fi
|
||||
|
||||
if (( ${#FAILED_PACKAGES[@]} + ${#BLOCKED_PACKAGES[@]} )); then
|
||||
if (( ${#FAILED_PACKAGES[@]} )); then
|
||||
echo "Failed packages:"
|
||||
printf ' - %s\n' "${FAILED_PACKAGES[@]}"
|
||||
fi
|
||||
if (( ${#BLOCKED_PACKAGES[@]} )); then
|
||||
echo "Packages blocked by failed dependencies:"
|
||||
printf ' - %s\n' "${BLOCKED_PACKAGES[@]}"
|
||||
fi
|
||||
print_warning "Some packages failed (see details above)"
|
||||
# Reserved for a completed run with unsuccessful packages. Other failures
|
||||
# must not let release publish arbitrary files left in the workspace.
|
||||
exit 2
|
||||
fi
|
||||
|
||||
print_success "Build completed successfully!"
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# Print the PR build matrix for a set of package directories as JSON: one
|
||||
# entry per package per supported architecture. Every package builds exactly
|
||||
# once, against edge, and that one artifact is what every channel ships:
|
||||
# channels are databases over a shared pool of files, and a filename must
|
||||
# mean one set of bytes. "channels" lists where the artifact is published on
|
||||
# merge: edge for everything, plus rc and stable immediately for the fast
|
||||
# ring. Eligibility comes from package_builds_for_mirror, the rule the
|
||||
# release host uses, so CI and the host cannot disagree.
|
||||
#
|
||||
# Usage: build-matrix [--arch <arch>|all] <package>...
|
||||
# Reads package names on stdin when none are given. With no --arch, every
|
||||
# architecture in CI_ARCHES (default "x86_64 aarch64") the package supports.
|
||||
# Output: {"include":[{"package":"x","arch":"x86_64","channels":"edge rc stable","publish_arches":"x86_64"},...]}
|
||||
# arch is where it builds; publish_arches lists every architecture
|
||||
# database the file goes into (all of them for arch=any).
|
||||
set -euo pipefail
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
source "$BUILD_ROOT/helpers/package-metadata.sh"
|
||||
|
||||
ARCHES=$CI_ARCHES
|
||||
if [[ "${1:-}" == --arch ]]; then [[ $2 == all ]] || ARCHES=$2; shift 2; fi
|
||||
for a in $ARCHES; do require_valid_arch "$a"; done
|
||||
|
||||
if (( $# )); then names=("$@"); else mapfile -t names; fi
|
||||
|
||||
entries=()
|
||||
for name in "${names[@]}"; do
|
||||
[[ -n "$name" ]] || continue
|
||||
pkgdir="$PKGBUILDS_DIR/$name"
|
||||
[[ -d "$pkgdir" ]] || continue
|
||||
# skip_build packages still build on their own PR (explicit --package
|
||||
# semantics); the host's unscoped runs are what skip them.
|
||||
channels=""
|
||||
for mirror in $VALID_MIRRORS; do
|
||||
package_builds_for_mirror "$pkgdir" "$mirror" && channels="$channels $mirror"
|
||||
done
|
||||
channels=${channels# }
|
||||
[[ -n "$channels" ]] || continue
|
||||
# An arch=any package produces one architecture-independent file, so it
|
||||
# builds once, on the first architecture, and that file serves every
|
||||
# channel database of every architecture.
|
||||
if [[ " $(package_arches "$pkgdir" "${ARCHES%% *}") " == *" any "* ]]; then
|
||||
entries+=("$(jq -nc --arg p "$name" --arg a "${ARCHES%% *}" --arg c "$channels" --arg pa "$ARCHES" '{package:$p, arch:$a, channels:$c, publish_arches:$pa}')")
|
||||
continue
|
||||
fi
|
||||
for arch in $ARCHES; do
|
||||
package_supports_arch "$pkgdir" "$arch" || continue
|
||||
entries+=("$(jq -nc --arg p "$name" --arg a "$arch" --arg c "$channels" '{package:$p, arch:$a, channels:$c, publish_arches:$a}')")
|
||||
done
|
||||
done
|
||||
|
||||
printf '%s\n' "${entries[@]}" | jq -sc '{include: .}'
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
# Build a reusable package environment from this checkout's own inputs.
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
|
||||
usage() {
|
||||
echo "Usage: bin/builder-image {key|build} [--arch x86_64|aarch64] [--mirror edge|rc|stable] [--tag IMAGE] [--fresh]"
|
||||
}
|
||||
|
||||
command=${1:-}
|
||||
[[ $# -eq 0 ]] || shift
|
||||
tag=""
|
||||
fresh=false
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
--arch) ARCH=${2:?Missing architecture}; shift 2 ;;
|
||||
--mirror) MIRROR=${2:?Missing mirror}; shift 2 ;;
|
||||
--tag) tag=${2:?Missing image tag}; shift 2 ;;
|
||||
--fresh) fresh=true; shift ;;
|
||||
*) usage >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
require_valid_arch "$ARCH"
|
||||
validate_mirror "$MIRROR" || { echo "Invalid mirror: $MIRROR" >&2; exit 1; }
|
||||
case "$command" in key|build) ;; *) usage >&2; exit 1 ;; esac
|
||||
if [[ $command == key && ( -n $tag || $fresh == true ) ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Include the whole build context, conservatively including mounted build
|
||||
# scripts too. Normalize timestamps and ownership so fresh checkouts agree;
|
||||
# retain file contents, names, executable bits and symlink targets. Bump v1
|
||||
# if the image build invocation or this compatibility contract changes.
|
||||
hash=$(tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner \
|
||||
--format=gnu -cf - -C "$BUILD_DIR" . | sha256sum | cut -d' ' -f1)
|
||||
key="v1-$ARCH-$MIRROR-$hash"
|
||||
if [[ $command == key ]]; then
|
||||
echo "$key"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/docker-helpers.sh"
|
||||
check_engine
|
||||
platform=$(get_platform_arg "$ARCH")
|
||||
tag=${tag:-omarchy-pkg-builder:latest-$ARCH-$MIRROR}
|
||||
revision=$(git -C "$BUILD_ROOT" rev-parse HEAD 2>/dev/null || echo unknown)
|
||||
args=("$platform" --build-arg "MIRROR=$MIRROR"
|
||||
--label "org.omarchy.builder.key=$key"
|
||||
--label "org.opencontainers.image.source=https://github.com/omacom/omarchy-pkgs"
|
||||
--label "org.opencontainers.image.revision=$revision"
|
||||
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
--tag "$tag" --file "$BUILD_DIR/Dockerfile")
|
||||
if [[ $fresh == true ]]; then
|
||||
# A daily build must refresh Arch even when its Dockerfile has not changed.
|
||||
args+=(--no-cache)
|
||||
if [[ $CONTAINER_ENGINE == docker ]]; then args+=(--pull); else args+=(--pull=always); fi
|
||||
fi
|
||||
if [[ $CONTAINER_ENGINE == docker ]]; then
|
||||
docker buildx build --load "${args[@]}" "$BUILD_DIR"
|
||||
else
|
||||
podman build "${args[@]}" "$BUILD_DIR"
|
||||
fi
|
||||
+63
-22
@@ -1,6 +1,10 @@
|
||||
#!/bin/bash
|
||||
# Check PKGBUILD versions against published repo versions
|
||||
# Creates state files for edge and/or stable if any packages need building
|
||||
# Creates a state file per channel and architecture when packages need building
|
||||
#
|
||||
# Usage: check-versions [--pull] [--arch <arch>]
|
||||
# --pull pull the repository first (the scheduled run does this)
|
||||
# --arch <arch> check one architecture; default: every published one
|
||||
|
||||
set -e
|
||||
|
||||
@@ -10,11 +14,27 @@ source "$BUILD_ROOT/helpers/paths.sh"
|
||||
source "$BUILD_ROOT/helpers/package-metadata.sh"
|
||||
source "$BUILD_ROOT/helpers/lock-helpers.sh"
|
||||
|
||||
STATE_DIR="${OMARCHY_STATE_DIR:-/root/.state}"
|
||||
ARCH="${ARCH:-x86_64}"
|
||||
PULL=false
|
||||
ARCHES=""
|
||||
|
||||
[[ "${1:-}" == "--pull" ]] && PULL=true
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--pull)
|
||||
PULL=true
|
||||
shift
|
||||
;;
|
||||
--arch)
|
||||
require_valid_arch "$2"
|
||||
ARCHES="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$ARCHES" ]] || ARCHES=$(published_arches)
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
@@ -59,10 +79,26 @@ get_repo_version() {
|
||||
'
|
||||
}
|
||||
|
||||
# The PKGBUILD's full version (epoch:pkgver-pkgrel) for the architecture being
|
||||
# checked. Prints nothing when pkgver or pkgrel cannot be read: a partial
|
||||
# version like "-" would compare unequal to everything published and queue a
|
||||
# rebuild on every tick, so an unreadable PKGBUILD must not queue at all.
|
||||
get_pkgbuild_version() {
|
||||
local pkgdir="$1"
|
||||
if [[ -f "$pkgdir/PKGBUILD" ]]; then
|
||||
(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; if [[ -n "$epoch" ]]; then echo "${epoch}:${pkgver}-${pkgrel}"; else echo "${pkgver}-${pkgrel}"; fi')
|
||||
local epoch pkgver pkgrel
|
||||
|
||||
[[ -f "$pkgdir/PKGBUILD" ]] || return 1
|
||||
|
||||
epoch=$(package_pkgbuild_var "$pkgdir" epoch "$ARCH")
|
||||
pkgver=$(package_pkgbuild_var "$pkgdir" pkgver "$ARCH")
|
||||
pkgrel=$(package_pkgbuild_var "$pkgdir" pkgrel "$ARCH")
|
||||
|
||||
[[ -n "$pkgver" && -n "$pkgrel" ]] || return 1
|
||||
|
||||
if [[ -n "$epoch" ]]; then
|
||||
echo "${epoch}:${pkgver}-${pkgrel}"
|
||||
else
|
||||
echo "${pkgver}-${pkgrel}"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -77,8 +113,8 @@ check_vcs_unchanged() {
|
||||
|
||||
# If epoch or pkgrel changed in PKGBUILD, release even if upstream is unchanged.
|
||||
local pkgbuild_epoch pkgbuild_pkgrel
|
||||
pkgbuild_epoch=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${epoch:-}"')
|
||||
pkgbuild_pkgrel=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${pkgrel}"')
|
||||
pkgbuild_epoch=$(package_pkgbuild_var "$pkgdir" epoch "$ARCH")
|
||||
pkgbuild_pkgrel=$(package_pkgbuild_var "$pkgdir" pkgrel "$ARCH")
|
||||
|
||||
local repo_pkgrel="${repo_version##*-}"
|
||||
local repo_no_pkgrel="${repo_version%-*}"
|
||||
@@ -106,12 +142,11 @@ check_package() {
|
||||
local mirror="$3"
|
||||
|
||||
local pkgbuild_version repo_version
|
||||
pkgbuild_version=$(get_pkgbuild_version "$pkgdir")
|
||||
repo_version=$(get_repo_version "$pkg" "$mirror")
|
||||
|
||||
if [[ -z "$pkgbuild_version" ]]; then
|
||||
if ! pkgbuild_version=$(get_pkgbuild_version "$pkgdir") || [[ -z "$pkgbuild_version" ]]; then
|
||||
print_warning "$pkg: could not read pkgver/pkgrel from its PKGBUILD for $ARCH — not queueing"
|
||||
return 1
|
||||
fi
|
||||
repo_version=$(get_repo_version "$pkg" "$mirror")
|
||||
|
||||
if grep -qE '^pkgver[[:space:]]*\(\)' "$pkgdir/PKGBUILD"; then
|
||||
if [[ -n "$repo_version" && -n "$(package_extract_vcs_hash_from_version "$repo_version")" ]]; then
|
||||
@@ -137,8 +172,8 @@ check_package() {
|
||||
# it because that exact filename is already published with different bytes.
|
||||
# If the artifact for this version already exists in the channel, there is
|
||||
# nothing to build regardless of which direction the versions differ.
|
||||
if compgen -G "$REPO_ROOT/$mirror/$ARCH/${pkg}-${pkgbuild_version}-*.pkg.tar."[!s]* >/dev/null 2>&1; then
|
||||
print_warning "$pkg $pkgbuild_version is already published — this checkout is behind the channel; not queueing"
|
||||
if package_version_is_published "$REPO_ROOT/$mirror/$ARCH" "$pkg" "$pkgbuild_version" "$ARCH"; then
|
||||
print_warning "$pkg $pkgbuild_version is already published; not queueing"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -147,11 +182,12 @@ check_package() {
|
||||
|
||||
check_mirror() {
|
||||
local mirror="$1"
|
||||
local state_file="$STATE_DIR/.sync-needed-$mirror"
|
||||
local state_file
|
||||
state_file=$(sync_queue_file "$mirror" "$ARCH")
|
||||
local needs_build=false
|
||||
local packages=()
|
||||
|
||||
print_info "Checking $mirror packages..."
|
||||
print_info "Checking $mirror packages for $ARCH..."
|
||||
|
||||
while IFS= read -r pkg; do
|
||||
local pkgdir="$PKGBUILDS_DIR/$pkg"
|
||||
@@ -159,7 +195,7 @@ check_mirror() {
|
||||
needs_build=true
|
||||
packages+=("$pkg")
|
||||
fi
|
||||
done < <(packages_for_unscoped_build "$mirror")
|
||||
done < <(packages_for_unscoped_build "$mirror" "$ARCH")
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -168,18 +204,23 @@ check_mirror() {
|
||||
# this to name the packages in its start report, which is the difference
|
||||
# between "a build is running" and "your package is in this build".
|
||||
printf '%s\n' "${packages[@]}" >"$state_file"
|
||||
print_success "${mirror^} needs building (${#packages[@]} packages)"
|
||||
print_success "${mirror^} ($ARCH) needs building (${#packages[@]} packages)"
|
||||
print_info "Packages: ${packages[*]}"
|
||||
print_info "State file created: $state_file"
|
||||
else
|
||||
print_info "${mirror^} is up to date"
|
||||
print_info "${mirror^} ($ARCH) is up to date"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
check_mirror edge
|
||||
check_mirror rc
|
||||
check_mirror stable
|
||||
# One pass per published architecture: the version comparison reads that
|
||||
# architecture's channel databases, and each queue is its own file.
|
||||
for ARCH in $ARCHES; do
|
||||
update_arch_paths
|
||||
check_mirror edge
|
||||
check_mirror rc
|
||||
check_mirror stable
|
||||
done
|
||||
|
||||
print_success "Version check complete!"
|
||||
+13
-7
@@ -1,21 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Clean Docker builder images and cache
|
||||
# Clean builder images and cache for the selected container engine
|
||||
# Forces a fresh image build on next run
|
||||
|
||||
set -e
|
||||
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/docker-helpers.sh"
|
||||
|
||||
print_header "Cleaning Docker Builder Images"
|
||||
check_engine
|
||||
print_header "Cleaning Container Builder Images"
|
||||
|
||||
# Remove all omarchy-pkg-builder images
|
||||
print_info "Removing omarchy-pkg-builder images..."
|
||||
docker images omarchy-pkg-builder -q | xargs -r docker rmi -f 2>/dev/null || true
|
||||
"$CONTAINER_ENGINE" images omarchy-pkg-builder -q | xargs -r "$CONTAINER_ENGINE" rmi -f 2>/dev/null || true
|
||||
|
||||
# Clear buildx cache for these builds
|
||||
print_info "Clearing buildx cache..."
|
||||
docker buildx prune -a -f
|
||||
# Clear this engine's build cache.
|
||||
print_info "Clearing build cache..."
|
||||
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||
"$CONTAINER_ENGINE" buildx prune -a -f
|
||||
else
|
||||
"$CONTAINER_ENGINE" image prune --build-cache -f
|
||||
fi
|
||||
|
||||
print_success "Docker builder cache cleared"
|
||||
print_success "Container builder cache cleared"
|
||||
print_info "Next build will create fresh images"
|
||||
+7
-3
@@ -31,9 +31,13 @@ clean_packages() {
|
||||
# Skip signature files
|
||||
[[ "$pkg" == *.sig ]] && continue
|
||||
|
||||
# Extract package name (remove version and architecture)
|
||||
# Format: name-version-release-arch.pkg.tar.*
|
||||
local pkgname=$(echo "$pkg" | sed -E 's/-[0-9]+.*-(any|x86_64|i686)\.pkg\.tar\..*//')
|
||||
# Format: name-version-release-arch.pkg.tar.*. Work from the right because
|
||||
# package names can themselves contain version-like pieces (qt6-5compat,
|
||||
# nvidia-580xx-utils), while pkgver and pkgrel cannot contain hyphens.
|
||||
local stem="${pkg%%.pkg.tar.*}"
|
||||
stem="${stem%-*}" # architecture
|
||||
stem="${stem%-*}" # pkgrel
|
||||
local pkgname="${stem%-*}" # pkgver
|
||||
|
||||
# Add to array
|
||||
if [[ -n "${packages[$pkgname]}" ]]; then
|
||||
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# Internal initial-recipe import used by add-package. Maintained recipes are
|
||||
# never replaced; subsequent releases go through sync-upstream.
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
|
||||
if [[ ${1:-} == --help || ${1:-} == -h ]]; then
|
||||
echo "Usage: bin/add-package <package> --source aur [--aur <upstream-name>]"
|
||||
exit 0
|
||||
fi
|
||||
if [[ $# != 1 || ! $1 =~ ^[a-zA-Z0-9@_+][a-zA-Z0-9@._+-]*$ ]]; then
|
||||
print_error "Use bin/add-package <package> --source aur for an initial import"
|
||||
exit 1
|
||||
fi
|
||||
package=$1
|
||||
package_dir="$PKGBUILDS_DIR/$package"
|
||||
metadata="$package_dir/.omarchy/package.json"
|
||||
if [[ -f "$package_dir/PKGBUILD" ]]; then
|
||||
print_error "Refusing to replace the maintained recipe for $package"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$metadata" ]] || [[ $(jq -r .source "$metadata") != aur ]]; then
|
||||
print_error "Initial import requires metadata created by bin/add-package --source aur"
|
||||
exit 1
|
||||
fi
|
||||
aur_package=$(jq -r --arg name "$package" '.aur // $name' "$metadata")
|
||||
if [[ ! $aur_package =~ ^[a-zA-Z0-9@_+][a-zA-Z0-9@._+-]*$ ]]; then
|
||||
print_error "Invalid AUR package name"
|
||||
exit 1
|
||||
fi
|
||||
# Refuse unrelated package files: an import only starts from .omarchy metadata.
|
||||
shopt -s dotglob nullglob
|
||||
for item in "$package_dir"/*; do
|
||||
if [[ ${item##*/} != .omarchy ]]; then
|
||||
print_error "Initial import needs an empty package directory: $package_dir"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
work=$(mktemp -d "$PKGBUILDS_DIR/.import-aur.XXXXXX")
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
print_info "Importing $package from AUR package $aur_package..."
|
||||
git clone --quiet "https://aur.archlinux.org/${aur_package}.git" "$work/recipe"
|
||||
[[ -f "$work/recipe/PKGBUILD" ]] || { print_error "AUR package has no PKGBUILD"; exit 1; }
|
||||
commit=$(git -C "$work/recipe" rev-parse HEAD)
|
||||
rm -rf "$work/recipe/.git" "$work/recipe/.omarchy"
|
||||
rm -f "$work/recipe/.SRCINFO" "$work/recipe/.gitignore"
|
||||
cp -a "$package_dir/.omarchy" "$work/recipe/.omarchy"
|
||||
jq --arg name "$aur_package" --arg commit "$commit" '
|
||||
.source = "local" | .origin = {aur: $name, commit: $commit}
|
||||
| del(.aur, .upstream_commit)
|
||||
' "$metadata" > "$work/recipe/.omarchy/package.json"
|
||||
# Stage on the same filesystem, restoring the metadata directory on failure.
|
||||
mv "$package_dir" "$work/original"
|
||||
if ! mv "$work/recipe" "$package_dir"; then
|
||||
mv "$work/original" "$package_dir"
|
||||
exit 1
|
||||
fi
|
||||
print_success "Imported $package; review the recipe and configure its direct upstream watch"
|
||||
+14
-7
@@ -21,9 +21,10 @@ BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
source "$BUILD_ROOT/helpers/host-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/package-metadata.sh"
|
||||
|
||||
UPSTREAM_URL="${OMARCHY_UPSTREAM_URL:-https://github.com/basecamp/omarchy.git}"
|
||||
EDGE_DB_URL="${OMARCHY_EDGE_DB_URL:-https://pkgs.omarchy.org/edge/x86_64/omarchy.db.tar.zst}"
|
||||
EDGE_DB_URL="${OMARCHY_EDGE_DB_URL:-https://pkgs.omarchy.org/edge/$(reference_arch)/omarchy.db.tar.zst}"
|
||||
RELEASE_PACKAGES=(omarchy omarchy-settings)
|
||||
DEFAULT_RC_REF="quattro"
|
||||
SRCDEST_DIR="$BUILD_ROOT/.srcdest"
|
||||
@@ -92,7 +93,7 @@ version_is_rc() { version_is_prerelease "$1"; }
|
||||
|
||||
pkgbuild_var() {
|
||||
local pkg="$1" var="$2"
|
||||
(cd "$BUILD_ROOT/pkgbuilds/$pkg" && bash -c "source PKGBUILD 2>/dev/null; echo \"\${$var}\"")
|
||||
package_pkgbuild_var "$BUILD_ROOT/pkgbuilds/$pkg" "$var"
|
||||
}
|
||||
|
||||
# Prints the published version of $pkg from the edge DB. Distinguishes
|
||||
@@ -327,13 +328,19 @@ regenerate_checksums() {
|
||||
|
||||
trigger_build_host() {
|
||||
local host
|
||||
queue_edge_builds() {
|
||||
mkdir -p "$STATE_DIR" || return 1
|
||||
local arch
|
||||
for arch in $(published_arches); do
|
||||
touch "$(sync_queue_file edge "$arch")" || return 1
|
||||
done
|
||||
}
|
||||
|
||||
# Explicit host configuration outranks the local-host inference (a
|
||||
# workstation that ran a full local release carries the db marker too).
|
||||
if ! resolve_repo_host "${REPO_HOST_OVERRIDE:-}" >/dev/null && on_repo_host; then
|
||||
print_info "Triggering edge build locally (this is the build host)..."
|
||||
if mkdir -p "${OMARCHY_STATE_DIR:-/root/.state}" &&
|
||||
touch "${OMARCHY_STATE_DIR:-/root/.state}/.sync-needed-edge" &&
|
||||
systemctl start --no-block omarchy-auto-release-edge.service; then
|
||||
if queue_edge_builds && systemctl start --no-block omarchy-auto-release-edge.service; then
|
||||
print_success "Edge build triggered"
|
||||
else
|
||||
print_warning "Could not start the edge release service — the 6-hourly timer will pick it up"
|
||||
@@ -343,11 +350,11 @@ trigger_build_host() {
|
||||
if ! host=$(resolve_repo_host "${REPO_HOST_OVERRIDE:-}"); then
|
||||
print_info "No repository host configured (set OMARCHY_REPO_HOST or $BUILD_ROOT/.repo-host — any ssh destination, e.g. root@<host> or an ssh-config alias)."
|
||||
print_info "The 6-hourly auto-release timer will pick this up, or trigger manually:"
|
||||
echo " ssh <build-host> 'git -C /root/omarchy-pkgs pull --ff-only && touch /root/.state/.sync-needed-edge && systemctl start omarchy-auto-release-edge.service'"
|
||||
echo " ssh <build-host> 'git -C /root/omarchy-pkgs pull --ff-only && systemctl start omarchy-check-versions.service omarchy-auto-release-edge.service'"
|
||||
return 0
|
||||
fi
|
||||
print_info "Triggering edge build on $host..."
|
||||
if ssh "$host" 'git -C /root/omarchy-pkgs pull --ff-only && mkdir -p /root/.state && touch /root/.state/.sync-needed-edge && systemctl start --no-block omarchy-auto-release-edge.service'; then
|
||||
if ssh "$host" 'git -C /root/omarchy-pkgs pull --ff-only && cd /root/omarchy-pkgs && export BUILD_ROOT=/root/omarchy-pkgs && source helpers/paths.sh && mkdir -p "$STATE_DIR" && for arch in $(published_arches); do touch "$(sync_queue_file edge "$arch")"; done && systemctl start --no-block omarchy-auto-release-edge.service'; then
|
||||
print_success "Edge build triggered on $host"
|
||||
else
|
||||
print_warning "Could not trigger $host — the 6-hourly timer will pick it up"
|
||||
|
||||
+93
-55
@@ -18,6 +18,7 @@ set -e
|
||||
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/docker-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
source "$BUILD_ROOT/helpers/host-helpers.sh"
|
||||
|
||||
@@ -28,7 +29,11 @@ ISO_REPO="${OMARCHY_ISO_REPO:-omacom-io/omarchy-iso}"
|
||||
DEV_BRANCH="${OMARCHY_DEV_BRANCH:-quattro}"
|
||||
|
||||
PKGS_DB_BASE="${OMARCHY_PKGS_DB_BASE:-https://pkgs.omarchy.org}"
|
||||
RC_DB_URL="$PKGS_DB_BASE/rc/x86_64/omarchy.db.tar.zst"
|
||||
# The first published architecture supplies the version-ordering floor when
|
||||
# cutting pins. Readiness checks below still verify every published
|
||||
# architecture before an RC or final release can move forward.
|
||||
OBSERVED_ARCH=$(reference_arch)
|
||||
RC_DB_URL="$PKGS_DB_BASE/rc/$OBSERVED_ARCH/omarchy.db.tar.zst"
|
||||
|
||||
SRCDEST_DIR="$BUILD_ROOT/.srcdest"
|
||||
MIRROR_CLONE="$SRCDEST_DIR/omarchy" # bare mirror (shared with bin/omarchy-pkgs)
|
||||
@@ -181,12 +186,12 @@ ensure_work_clone() {
|
||||
# Prints omarchy's published version in a channel; empty when absent, rc 2 when
|
||||
# the database cannot be read (callers must not mistake an outage for absence).
|
||||
published_version() {
|
||||
local channel="$1" tmp descs
|
||||
local channel="$1" arch="${2:-$OBSERVED_ARCH}" tmp descs
|
||||
tmp=$(mktemp) || return 2
|
||||
# A unique query string busts the CDN cache: right after a sync the plain
|
||||
# URL can keep serving the previous db for a while, which reads as "not
|
||||
# published yet" to status, the wait loop, and ship's pre-checks.
|
||||
if ! curl -sf "$PKGS_DB_BASE/$channel/x86_64/omarchy.db.tar.zst?$(date +%s%N)" -o "$tmp"; then
|
||||
if ! curl -sf "$PKGS_DB_BASE/$channel/$arch/omarchy.db.tar.zst?$(date +%s%N)" -o "$tmp"; then
|
||||
rm -f "$tmp"
|
||||
return 2
|
||||
fi
|
||||
@@ -207,6 +212,14 @@ published_version() {
|
||||
' <<<"$descs"
|
||||
}
|
||||
|
||||
all_arches_at_version() { # all_arches_at_version <channel> <pkgver>
|
||||
local channel="$1" want="$2" arch got
|
||||
for arch in $(published_arches); do
|
||||
got=$(published_version "$channel" "$arch" 2>/dev/null) || return 1
|
||||
[[ "${got%-*}" == "$want" ]] || return 1
|
||||
done
|
||||
}
|
||||
|
||||
# The rc branch of THIS repo carries the current pins. Read them without
|
||||
# touching the working tree.
|
||||
rc_branch_pin() { # prints "pkgver commit", empty when no rc branch
|
||||
@@ -251,8 +264,12 @@ fi
|
||||
git -C /root/omarchy-pkgs-rc fetch origin rc
|
||||
git -C /root/omarchy-pkgs-rc reset --hard origin/rc
|
||||
cd /root/omarchy-pkgs-rc
|
||||
OMARCHY_RC_PINS=1 OMARCHY_REPO_ROOT=/root/omarchy-pkgs/pkgs.omarchy.org \
|
||||
bin/repo release --mirror rc --package omarchy omarchy-settings --skip-prod-check
|
||||
# The pair is built once per published architecture (helpers/paths.sh on the
|
||||
# host decides which), so every architecture'"'"'s rc channel carries the pins.
|
||||
for arch in $(BUILD_ROOT=/root/omarchy-pkgs-rc bash -c "source helpers/paths.sh; published_arches"); do
|
||||
OMARCHY_RC_PINS=1 OMARCHY_REPO_ROOT=/root/omarchy-pkgs/pkgs.omarchy.org \
|
||||
bin/repo release --mirror rc --arch "$arch" --package omarchy omarchy-settings --skip-prod-check
|
||||
done
|
||||
'
|
||||
|
||||
trigger_rc_build() {
|
||||
@@ -278,19 +295,25 @@ host_advance() { # host_advance <from> <to> [extra args...]
|
||||
# against this machine's (likely stale) local tree would be wrong.
|
||||
if ! resolve_repo_host "$REPO_HOST_OVERRIDE" >/dev/null && ! on_repo_host; then
|
||||
print_no_host_help "advance $from -> $to" \
|
||||
" cd /root/omarchy-pkgs && bin/repo advance --from $from --to $to --skip-prod-check $*"
|
||||
" cd /root/omarchy-pkgs && bin/repo advance --from $from --to $to --arch all --skip-prod-check $*"
|
||||
return 1
|
||||
fi
|
||||
"$BUILD_ROOT/bin/repo" advance --from "$from" --to "$to" --skip-prod-check "$@"
|
||||
# --arch all: the host advances every architecture it publishes, so a train
|
||||
# never moves a channel for one architecture and not another.
|
||||
"$BUILD_ROOT/bin/repo" advance --from "$from" --to "$to" --arch all --skip-prod-check "$@"
|
||||
}
|
||||
|
||||
wait_for_published() { # wait_for_published <channel> <version> [timeout-seconds]
|
||||
local channel="$1" want="$2" timeout="${3:-3600}" waited=0 got
|
||||
print_info "Waiting for $want to appear in the $channel channel (up to $((timeout / 60))m)..."
|
||||
local channel="$1" want="$2" timeout="${3:-3600}" waited=0 arch got pending
|
||||
print_info "Waiting for $want in $channel for: $(published_arches | tr '\n' ' ')"
|
||||
while ((waited < timeout)); do
|
||||
got=$(published_version "$channel" 2>/dev/null) || got=""
|
||||
if [[ "${got%-*}" == "$want" ]]; then
|
||||
print_success "$channel now serves omarchy $got"
|
||||
pending=""
|
||||
for arch in $(published_arches); do
|
||||
got=$(published_version "$channel" "$arch" 2>/dev/null) || got=""
|
||||
[[ "${got%-*}" == "$want" ]] || pending+=" $arch=${got:-unreachable}"
|
||||
done
|
||||
if [[ -z "$pending" ]]; then
|
||||
print_success "$channel now serves omarchy $want on every published architecture"
|
||||
return 0
|
||||
fi
|
||||
sleep 60
|
||||
@@ -298,7 +321,7 @@ wait_for_published() { # wait_for_published <channel> <version> [timeout-seconds
|
||||
printf '.' >&2
|
||||
done
|
||||
echo "" >&2
|
||||
print_warning "Timed out waiting for $want in $channel (currently: ${got:-unknown})"
|
||||
print_warning "Timed out waiting for $want in $channel (pending:$pending)"
|
||||
print_info "The build may still be running — re-run this command to resume."
|
||||
return 1
|
||||
}
|
||||
@@ -352,8 +375,8 @@ iso_checkout() { # prints a usable omarchy-iso checkout, cloning to tmp if neede
|
||||
build_iso() { # build_iso <version> [--rc]
|
||||
local version="$1" rc_flag="${2:-}" dir
|
||||
dir=$(iso_checkout) || return 1
|
||||
if ! command -v docker >/dev/null || ! docker info >/dev/null 2>&1; then
|
||||
print_warning "Docker unavailable — cannot build the ISO here. Run on a Docker machine:"
|
||||
if [[ -z "$CONTAINER_ENGINE" ]] || ! "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
|
||||
print_warning "No container engine is available — cannot build the ISO here. Run on a Docker or Podman machine:"
|
||||
echo " cd $dir && bin/omarchy-iso-release ${rc_flag:+$rc_flag }$version"
|
||||
return 1
|
||||
fi
|
||||
@@ -662,15 +685,13 @@ cmd_rc() {
|
||||
if [[ -n "$pin" ]]; then
|
||||
local pin_ver="${pin%% *}" pin_commit="${pin##* }"
|
||||
if [[ "$pin_commit" == "$head" && "$pin_ver" =~ ^${version//./\\.}rc[0-9]+$ ]]; then
|
||||
local pub
|
||||
pub=$(published_version rc 2>/dev/null) || pub=""
|
||||
if [[ "${pub%-*}" == "$pin_ver" ]]; then
|
||||
print_success "$pin_ver is already cut from this head and published to rc"
|
||||
if all_arches_at_version rc "$pin_ver"; then
|
||||
print_success "$pin_ver is already cut from this head and published to rc on every architecture"
|
||||
maybe_iso "$pin_ver" rc "$iso_mode"
|
||||
return 0
|
||||
fi
|
||||
print_info "$pin_ver is pinned from this head but not published yet — re-triggering the build"
|
||||
trigger_rc_build || true
|
||||
trigger_rc_build || return 1
|
||||
[[ "$wait" == true ]] && wait_for_published rc "$pin_ver"
|
||||
maybe_iso "$pin_ver" rc "$iso_mode"
|
||||
return 0
|
||||
@@ -686,7 +707,7 @@ cmd_rc() {
|
||||
new_ver="${new_pin%% *}"
|
||||
print_success "Pinned $new_ver (rc branch pushed)"
|
||||
|
||||
trigger_rc_build || true
|
||||
trigger_rc_build || return 1
|
||||
if [[ "$wait" == true ]]; then
|
||||
wait_for_published rc "$new_ver" || return 1
|
||||
fi
|
||||
@@ -708,9 +729,7 @@ cmd_ship() {
|
||||
exit 1
|
||||
fi
|
||||
version=$(branch_to_version "$branch")
|
||||
local stable_now
|
||||
stable_now=$(published_version stable 2>/dev/null) || stable_now=""
|
||||
if [[ "${stable_now%-*}" == "$version" ]] &&
|
||||
if all_arches_at_version stable "$version" &&
|
||||
gh release view "v$version" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; then
|
||||
print_success "Nothing to ship — $version is tagged, released, and live on stable"
|
||||
exit 0
|
||||
@@ -752,10 +771,8 @@ cmd_ship() {
|
||||
echo " omarchy-release rc"
|
||||
exit 1
|
||||
fi
|
||||
local pub
|
||||
pub=$(published_version rc 2>/dev/null) || pub=""
|
||||
if [[ "${pub%-*}" != "$pin_ver" ]]; then
|
||||
print_error "$pin_ver is pinned but rc serves '${pub:-nothing}' — the candidate build hasn't published"
|
||||
if ! all_arches_at_version rc "$pin_ver"; then
|
||||
print_error "$pin_ver is pinned but is not published for every architecture"
|
||||
echo "Wait for it (or re-run: omarchy-release rc), then ship."
|
||||
exit 1
|
||||
fi
|
||||
@@ -799,10 +816,9 @@ cmd_ship() {
|
||||
|
||||
# 2. Final pins into rc. Resolve the tag we just established so the final
|
||||
# PKGBUILDs record both its provenance and its exact commit.
|
||||
local rc_pub stable_pub
|
||||
rc_pub=$(published_version rc 2>/dev/null) || rc_pub=""
|
||||
if [[ "${rc_pub%-*}" == "$version" ]]; then
|
||||
print_success "2/7 Final $version already published to rc"
|
||||
local stable_pub
|
||||
if all_arches_at_version rc "$version"; then
|
||||
print_success "2/7 Final $version already published to rc on every architecture"
|
||||
else
|
||||
if [[ "$pin_ver" != "$version" ]]; then
|
||||
print_info "2/7 Pinning final $version from tag v$version..."
|
||||
@@ -810,22 +826,21 @@ cmd_ship() {
|
||||
else
|
||||
print_info "2/7 Final $version pinned — re-triggering build"
|
||||
fi
|
||||
trigger_rc_build || true
|
||||
trigger_rc_build || exit 1
|
||||
wait_for_published rc "$version" || exit 1
|
||||
fi
|
||||
|
||||
# 3. Promote rc -> stable
|
||||
stable_pub=$(published_version stable 2>/dev/null) || stable_pub=""
|
||||
if [[ "${stable_pub%-*}" == "$version" ]]; then
|
||||
print_success "3/7 Stable already serves $version"
|
||||
if all_arches_at_version stable "$version"; then
|
||||
print_success "3/7 Stable already serves $version on every architecture"
|
||||
else
|
||||
host_advance rc stable || exit 1
|
||||
stable_pub=$(published_version stable 2>/dev/null) || stable_pub=""
|
||||
if [[ "${stable_pub%-*}" != "$version" ]]; then
|
||||
print_error "Promotion ran but stable serves '${stable_pub:-nothing}' — investigate before continuing"
|
||||
if ! all_arches_at_version stable "$version"; then
|
||||
print_error "Promotion ran but stable does not serve $version on every architecture"
|
||||
exit 1
|
||||
fi
|
||||
print_success "3/7 Promoted to stable: omarchy $stable_pub"
|
||||
stable_pub=$(published_version stable 2>/dev/null) || stable_pub="$version"
|
||||
print_success "3/7 Promoted to stable: omarchy $stable_pub on every architecture"
|
||||
fi
|
||||
|
||||
# 4. Final pins onto master (keeps edge overlap publishing and the repo record)
|
||||
@@ -908,7 +923,7 @@ next_step() { # prints "<command>|<description>"
|
||||
last=$(newest_release_branch 2>/dev/null) || last=""
|
||||
if [[ -n "$last" && "$STABLE_VER" != "<unreachable>" ]]; then
|
||||
last_ver=$(branch_to_version "$last")
|
||||
if [[ "${STABLE_VER%-*}" != "$last_ver" ]] ||
|
||||
if ! all_arches_at_version stable "$last_ver" ||
|
||||
{ command -v gh >/dev/null && ! gh release view "v$last_ver" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; }; then
|
||||
echo "ship|$last_ver is tagged but not fully shipped — resume ship"
|
||||
return
|
||||
@@ -924,7 +939,7 @@ next_step() { # prints "<command>|<description>"
|
||||
echo "ship|Final $TRAIN_VER is pinned — finish shipping (re-runs are safe)"
|
||||
elif [[ "$pin_commit" != "$TRAIN_HEAD" ]]; then
|
||||
echo "rc|$TRAIN has commits newer than $pin_ver — cut the next candidate"
|
||||
elif [[ "${RC_VER%-*}" != "$pin_ver" ]]; then
|
||||
elif ! all_arches_at_version rc "$pin_ver"; then
|
||||
echo "rc|$pin_ver is pinned but not published — re-run rc to re-trigger/wait"
|
||||
else
|
||||
echo "ship|$pin_ver is published to rc — test it, then ship"
|
||||
@@ -994,20 +1009,24 @@ cmd_doctor() {
|
||||
check "makepkg available (checksums)" command -v makepkg
|
||||
check "curl available" command -v curl
|
||||
check "upstream reachable ($UPSTREAM_URL)" git ls-remote "$UPSTREAM_URL" HEAD
|
||||
local ch
|
||||
local ch arch
|
||||
for ch in edge stable; do
|
||||
if published_version "$ch" >/dev/null 2>&1; then
|
||||
print_success "$ch channel db readable"
|
||||
for arch in $(published_arches); do
|
||||
if published_version "$ch" "$arch" >/dev/null 2>&1; then
|
||||
print_success "$ch/$arch channel db readable"
|
||||
else
|
||||
print_error "$ch/$arch channel db readable"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
done
|
||||
done
|
||||
for arch in $(published_arches); do
|
||||
if published_version rc "$arch" >/dev/null 2>&1; then
|
||||
print_success "rc/$arch channel db readable"
|
||||
else
|
||||
print_error "$ch channel db readable"
|
||||
failures=$((failures + 1))
|
||||
print_warning "rc/$arch channel db not readable — bootstrap it first: bin/repo bootstrap-rc (on the build host)"
|
||||
fi
|
||||
done
|
||||
if published_version rc >/dev/null 2>&1; then
|
||||
print_success "rc channel db readable"
|
||||
else
|
||||
print_warning "rc channel db not readable — bootstrap it first: bin/repo bootstrap-rc (on the build host)"
|
||||
fi
|
||||
local host
|
||||
if host=$(repo_host); then
|
||||
check "build host ssh ($host)" ssh -o ConnectTimeout=10 "$host" true
|
||||
@@ -1016,10 +1035,10 @@ cmd_doctor() {
|
||||
else
|
||||
print_warning "no build host configured — set OMARCHY_REPO_HOST, --host, or write an ssh destination (root@<host> or an ssh-config alias) to $BUILD_ROOT/.repo-host; until then builds trigger on the 6h timer only"
|
||||
fi
|
||||
if command -v docker >/dev/null && docker info >/dev/null 2>&1; then
|
||||
print_success "docker available (ISO builds possible here)"
|
||||
if [[ -n "$CONTAINER_ENGINE" ]] && "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
|
||||
print_success "$CONTAINER_ENGINE available (ISO builds possible here)"
|
||||
else
|
||||
print_warning "docker unavailable — ISO builds will print instructions instead"
|
||||
print_warning "container engine unavailable — ISO builds will print instructions instead"
|
||||
fi
|
||||
echo ""
|
||||
if ((failures == 0)); then
|
||||
@@ -1054,6 +1073,25 @@ cmd_self_test() {
|
||||
expect "version_is_patch 5.0.0" "$(version_is_patch 5.0.0 && echo yes || echo no)" "no"
|
||||
expect "previous_patch_tag 4.0.2" "$(previous_patch_tag 4.0.2)" "v4.0.1"
|
||||
expect "previous_patch_tag 4.0.10" "$(previous_patch_tag 4.0.10)" "v4.0.9"
|
||||
|
||||
# Keep release readiness fail-closed when only one architecture has reached
|
||||
# the requested version. This replaces the network reader for this process;
|
||||
# self-test exits immediately afterwards.
|
||||
published_version() {
|
||||
case "$2" in
|
||||
x86_64) echo "${TEST_X86_VERSION:-4.0.2-1}" ;;
|
||||
aarch64) echo "${TEST_ARM_VERSION:-4.0.2-1}" ;;
|
||||
esac
|
||||
}
|
||||
PUBLISHED_ARCHES=x86_64
|
||||
expect "x86-only readiness" "$(all_arches_at_version rc 4.0.2 && echo ready || echo blocked)" "ready"
|
||||
PUBLISHED_ARCHES=aarch64
|
||||
expect "ARM-only readiness" "$(all_arches_at_version rc 4.0.2 && echo ready || echo blocked)" "ready"
|
||||
PUBLISHED_ARCHES="x86_64 aarch64"
|
||||
TEST_ARM_VERSION=4.0.1-1
|
||||
expect "mixed versions block release" "$(all_arches_at_version rc 4.0.2 && echo ready || echo blocked)" "blocked"
|
||||
TEST_ARM_VERSION=4.0.2-1
|
||||
expect "both architectures ready" "$(all_arches_at_version rc 4.0.2 && echo ready || echo blocked)" "ready"
|
||||
echo ""
|
||||
if ((failures == 0)); then
|
||||
print_success "Self-test passed"
|
||||
|
||||
@@ -17,13 +17,13 @@ Usage: $0 <package> [OPTIONS]
|
||||
Create a scratch workspace for inspecting an AUR-backed package.
|
||||
|
||||
The workspace contains:
|
||||
upstream/ Raw AUR package at .omarchy/package.json upstream_commit, or HEAD
|
||||
upstream/ Raw AUR package at the recorded origin.commit, or HEAD
|
||||
patched/ Raw AUR package with Omarchy .omarchy patches/hooks/pkgrel applied
|
||||
current/ Current checked-in package directory
|
||||
|
||||
Options:
|
||||
--dir <path> Workspace directory (default: mktemp under /tmp)
|
||||
--commit <sha> Use a specific AUR commit instead of upstream_commit
|
||||
--commit <sha> Use a specific AUR commit instead of origin.commit
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
@@ -75,13 +75,13 @@ if [[ ! -f "$METADATA" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(jq -r '.source // ""' "$METADATA")" != "aur" ]]; then
|
||||
if [[ "$(jq -r '.origin.aur // .aur // (if .source == "aur" then "legacy" else "" end)' "$METADATA")" == "" ]]; then
|
||||
print_error "package-worktree only supports AUR-backed packages"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
AUR_PACKAGE=$(jq -r --arg package "$PACKAGE" '.aur // $package' "$METADATA")
|
||||
UPSTREAM_COMMIT=${COMMIT_OVERRIDE:-$(jq -r '.upstream_commit // ""' "$METADATA")}
|
||||
AUR_PACKAGE=$(jq -r --arg package "$PACKAGE" '.origin.aur // .aur // $package' "$METADATA")
|
||||
UPSTREAM_COMMIT=${COMMIT_OVERRIDE:-$(jq -r '.origin.commit // .upstream_commit // ""' "$METADATA")}
|
||||
|
||||
if [[ -z "$DEST_DIR" ]]; then
|
||||
DEST_DIR=$(mktemp -d "${TMPDIR:-/tmp}/omarchy-${PACKAGE}.XXXXXX")
|
||||
@@ -224,7 +224,7 @@ print_info "AUR package: $AUR_PACKAGE"
|
||||
if [[ -n "$UPSTREAM_COMMIT" ]]; then
|
||||
print_info "Upstream commit: $UPSTREAM_COMMIT"
|
||||
else
|
||||
print_warning "No upstream_commit recorded; using AUR HEAD"
|
||||
print_warning "No origin.commit recorded; using AUR HEAD"
|
||||
fi
|
||||
|
||||
rm -rf "$UPSTREAM_DIR" "$PATCHED_DIR" "$CURRENT_DIR"
|
||||
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
# Publish built packages into one channel of the remote repository,
|
||||
# incrementally and immutably.
|
||||
#
|
||||
# publish-artifact --mirror <edge|rc|stable> --arch <arch> <pkg files...>
|
||||
#
|
||||
# What it does, in order:
|
||||
# 1. pull the channel's current database from the remote
|
||||
# 2. refuse if any package filename already exists on the remote
|
||||
# 3. sign each package (GPG_PRIVATE_KEY / GPG_PASSPHRASE)
|
||||
# 4. repo-add the packages into the pulled database (replaces the entry
|
||||
# for that name; nothing else in the channel is touched)
|
||||
# 5. upload packages, then signatures, then the database last
|
||||
#
|
||||
# Never overwrites: uploads use --ignore-existing for packages and the
|
||||
# pre-check in step 2 makes a same-name collision a hard failure rather than
|
||||
# a silent skip. The database is the only object rewritten, and it is
|
||||
# uploaded only after every file it references is present.
|
||||
#
|
||||
# The remote is an rclone remote (REMOTE, default the production one);
|
||||
# OMARCHY_PUBLISH_PREFIX can point a proof run at a scratch prefix.
|
||||
set -euo pipefail
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
|
||||
REMOTE=${REMOTE:-pkgs.omarchy.org:omarchy-pkgs}
|
||||
PREFIX=${OMARCHY_PUBLISH_PREFIX:-}
|
||||
FILES=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--mirror) MIRROR=$2; require_valid_mirror "$MIRROR"; shift 2 ;;
|
||||
--arch) ARCH=$2; require_valid_arch "$ARCH"; shift 2 ;;
|
||||
--remote) REMOTE=$2; shift 2 ;;
|
||||
-h|--help) sed -n '2,22p' "$0"; exit 0 ;;
|
||||
-*) print_error "Unknown option: $1"; exit 1 ;;
|
||||
*) FILES+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
(( ${#FILES[@]} )) || { print_error "No package files given"; exit 1; }
|
||||
: "${GPG_PRIVATE_KEY:?}"; GPG_PASSPHRASE=${GPG_PASSPHRASE-}
|
||||
|
||||
DEST="$REMOTE/${PREFIX:+$PREFIX/}$MIRROR/$ARCH"
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
print_header "Publish to $DEST"
|
||||
|
||||
# --- 0. sanity: every file is a package, named as makepkg names it ---------
|
||||
for f in "${FILES[@]}"; do
|
||||
[[ -f "$f" && "$f" == *.pkg.tar.* && "$f" != *.sig ]] || { print_error "Not a package file: $f"; exit 1; }
|
||||
name=$(bsdtar -xOf "$f" .PKGINFO | awk -F' = ' '$1=="pkgname"{print $2}')
|
||||
ver=$(bsdtar -xOf "$f" .PKGINFO | awk -F' = ' '$1=="pkgver"{print $2}')
|
||||
pkgarch=$(bsdtar -xOf "$f" .PKGINFO | awk -F' = ' '$1=="arch"{print $2}')
|
||||
[[ "$(basename "$f")" == "$name-$ver-$pkgarch.pkg.tar."* ]] || {
|
||||
print_error "Filename does not match PKGINFO ($name-$ver-$pkgarch): $(basename "$f")"; exit 1; }
|
||||
[[ "$pkgarch" == any || "$pkgarch" == "$ARCH" ]] || { print_error "$f is $pkgarch, publishing to $ARCH"; exit 1; }
|
||||
done
|
||||
|
||||
# --- 1. pull the current database -----------------------------------------
|
||||
mkdir -p "$WORK/repo"
|
||||
listing=$(rclone lsf "$DEST/" --s3-no-head 2>/dev/null || true)
|
||||
if grep -q '^omarchy.db.tar.zst$' <<<"$listing"; then
|
||||
rclone copy "$DEST/omarchy.db.tar.zst" "$WORK/repo" --s3-no-head
|
||||
rclone copy "$DEST/omarchy.files.tar.zst" "$WORK/repo" --s3-no-head 2>/dev/null || true
|
||||
print_info "Pulled current database ($(tar -tf "$WORK/repo/omarchy.db.tar.zst" | grep -c '/$') entries)"
|
||||
else
|
||||
print_warning "No database at $DEST — creating a new one"
|
||||
fi
|
||||
|
||||
# --- 2. same-name collisions ----------------------------------------------
|
||||
# A filename must mean one set of bytes across every channel. The same file
|
||||
# reaching a channel that already holds it (a fast-ring publish after edge,
|
||||
# a re-run, a later promotion) is fine: it is skipped on upload and only the
|
||||
# database entry is added. Different bytes under a name the channel already
|
||||
# has is the one thing this must never do.
|
||||
for f in "${FILES[@]}"; do
|
||||
b=$(basename "$f")
|
||||
grep -qxF "$b" <<<"$listing" || continue
|
||||
remote_sum=$(rclone hashsum md5 "$DEST/$b" --s3-no-head 2>/dev/null | awk '{print $1}')
|
||||
local_sum=$(md5sum "$f" | awk '{print $1}')
|
||||
if [[ -n "$remote_sum" && "$remote_sum" == "$local_sum" ]]; then
|
||||
print_info "Already published with identical bytes, adding to the database only: $b"
|
||||
else
|
||||
print_error "Already published with DIFFERENT bytes, refusing to overwrite: $b"
|
||||
echo " Bump pkgrel; published filenames are immutable."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# --- 3. sign ---------------------------------------------------------------
|
||||
export GNUPGHOME="$WORK/gnupg"; mkdir -m 700 "$GNUPGHOME"
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --quiet --import
|
||||
KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '$1=="sec"{print $5; exit}')
|
||||
[[ -n "$KEY_ID" ]] || { print_error "No secret key imported"; exit 1; }
|
||||
for f in "${FILES[@]}"; do
|
||||
cp "$f" "$WORK/repo/"
|
||||
gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" \
|
||||
--detach-sign --no-armor --local-user "$KEY_ID" "$WORK/repo/$(basename "$f")"
|
||||
print_step "signed $(basename "$f")"
|
||||
done
|
||||
|
||||
# --- 4. repo-add (replaces the entry for each pkgname) ---------------------
|
||||
( cd "$WORK/repo" && repo-add --quiet omarchy.db.tar.zst "${FILES[@]/#*\//}" )
|
||||
ln -sf omarchy.db.tar.zst "$WORK/repo/omarchy.db"
|
||||
ln -sf omarchy.files.tar.zst "$WORK/repo/omarchy.files"
|
||||
print_info "Database now has $(tar -tf "$WORK/repo/omarchy.db.tar.zst" | grep -c '/$') entries"
|
||||
|
||||
# --- 5. upload: packages, signatures, database last -----------------------
|
||||
rclone copy "$WORK/repo" "$DEST" --s3-no-head --ignore-existing --filter '- *.sig' --filter '+ *.pkg.tar.*' --filter '- *'
|
||||
rclone copy "$WORK/repo" "$DEST" --s3-no-head --ignore-existing --filter '+ *.pkg.tar.*.sig' --filter '- *'
|
||||
# Re-verify every referenced file is really there before the db goes up.
|
||||
listing=$(rclone lsf "$DEST/" --s3-no-head)
|
||||
for f in "${FILES[@]}"; do
|
||||
b=$(basename "$f")
|
||||
grep -qxF "$b" <<<"$listing" && grep -qxF "$b.sig" <<<"$listing" || { print_error "Upload incomplete: $b"; exit 1; }
|
||||
done
|
||||
rclone copy "$WORK/repo" "$DEST" --s3-no-head --checksum --copy-links --filter '+ omarchy.db*' --filter '+ omarchy.files*' --filter '- *'
|
||||
print_success "Published ${#FILES[@]} package(s) to $DEST"
|
||||
+19
-8
@@ -149,11 +149,16 @@ fi
|
||||
# leave nvidia-580xx-dkms and opencl-nvidia-580xx behind. An output's own name
|
||||
# still matches, for pushing just one of them on purpose.
|
||||
#
|
||||
# pkgbase comes from .PKGINFO rather than the PKGBUILD: it is what makepkg
|
||||
# actually recorded, and it needs no guessing about which directory built what.
|
||||
pkgbase_of() {
|
||||
# Package identity comes from .PKGINFO rather than the filename or PKGBUILD: it
|
||||
# is what makepkg actually recorded, and it handles epochs and split packages.
|
||||
package_identity_of() {
|
||||
bsdtar -xOf "$1" .PKGINFO 2>/dev/null |
|
||||
awk -F ' = ' '$1 == "pkgbase" { print $2; exit }'
|
||||
awk -F ' = ' '
|
||||
$1 == "pkgname" { name = $2 }
|
||||
$1 == "pkgbase" { base = $2 }
|
||||
$1 == "pkgver" { version = $2 }
|
||||
END { print name "\t" base "\t" version }
|
||||
'
|
||||
}
|
||||
|
||||
FILES=()
|
||||
@@ -161,14 +166,18 @@ if [[ -z "$PACKAGES" ]]; then
|
||||
FILES=("${ALL_FILES[@]}")
|
||||
else
|
||||
declare -A MATCHED=()
|
||||
declare -A LATEST_FILE=()
|
||||
declare -A LATEST_VERSION=()
|
||||
for file in "${ALL_FILES[@]}"; do
|
||||
# name-version-release-arch.pkg.tar.zst -> name
|
||||
pkgname="${file%-*-*-*.pkg.tar.*}"
|
||||
pkgbase=$(pkgbase_of "$BUILD_OUTPUT_DIR/$file")
|
||||
IFS=$'\t' read -r pkgname pkgbase pkgver < <(package_identity_of "$BUILD_OUTPUT_DIR/$file")
|
||||
for wanted in $PACKAGES; do
|
||||
if [[ "$pkgname" == "$wanted" || "$pkgbase" == "$wanted" ]]; then
|
||||
FILES+=("$file")
|
||||
MATCHED["$wanted"]=1
|
||||
if [[ -z "${LATEST_FILE[$pkgname]:-}" ]] ||
|
||||
[[ $(vercmp "$pkgver" "${LATEST_VERSION[$pkgname]}") -gt 0 ]]; then
|
||||
LATEST_FILE["$pkgname"]="$file"
|
||||
LATEST_VERSION["$pkgname"]="$pkgver"
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
@@ -181,6 +190,8 @@ else
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
mapfile -t FILES < <(printf '%s\n' "${LATEST_FILE[@]}" | sort)
|
||||
fi
|
||||
|
||||
if [[ ${#FILES[@]} -eq 0 ]]; then
|
||||
|
||||
+61
-4
@@ -114,7 +114,11 @@ BUILT_FILES=""
|
||||
# What the scheduled version check queued, when it was the one that asked for
|
||||
# this run. Absent for a manual run, which is fine — the report just omits it.
|
||||
QUEUED_PACKAGES=""
|
||||
QUEUE_FILE="${OMARCHY_STATE_DIR:-/root/.state}/.sync-needed-$MIRROR"
|
||||
QUEUE_FILE=$(sync_queue_file "$MIRROR" "$ARCH")
|
||||
# A queue written under the pre-architecture name belongs to x86_64.
|
||||
if [[ "$ARCH" == "x86_64" && ! -s "$QUEUE_FILE" && -s "$(legacy_sync_queue_file "$MIRROR")" ]]; then
|
||||
QUEUE_FILE=$(legacy_sync_queue_file "$MIRROR")
|
||||
fi
|
||||
[[ -s "$QUEUE_FILE" ]] && QUEUED_PACKAGES=$(grep -c '' "$QUEUE_FILE")
|
||||
|
||||
if [[ "$DRY_RUN" != true ]]; then
|
||||
@@ -134,11 +138,43 @@ if [[ "$DRY_RUN" == true ]]; then
|
||||
else
|
||||
print_info "Step 1/6: Building packages..."
|
||||
fi
|
||||
"$BUILD_ROOT/bin/build" "${BUILD_ARGS[@]}" || {
|
||||
BUILD_RESULT_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$BUILD_RESULT_DIR"' EXIT
|
||||
build_status=0
|
||||
OMARCHY_BUILD_RESULT_DIR="$BUILD_RESULT_DIR" "$BUILD_ROOT/bin/build" "${BUILD_ARGS[@]}" || build_status=$?
|
||||
partial=false
|
||||
if [[ "$build_status" == 2 && -f "$BUILD_RESULT_DIR/complete" && "$DRY_RUN" != true ]]; then
|
||||
if [[ "${OMARCHY_DEFER_RUNTIME_DEPS:-false}" == true ]]; then
|
||||
print_error "The deferred release pair must succeed together; nothing will be published"
|
||||
notify_error "Release failed: Incomplete release pair" "$RELEASE_CONTEXT"
|
||||
exit 1
|
||||
fi
|
||||
partial=true
|
||||
while IFS= read -r file; do
|
||||
[[ -f "$BUILD_OUTPUT_DIR/$file" ]] || {
|
||||
print_error "Completed build artifact is missing: $file"
|
||||
notify_error "Release failed: Missing completed artifact" "$RELEASE_CONTEXT"
|
||||
exit 1
|
||||
}
|
||||
done < "$BUILD_RESULT_DIR/artifacts"
|
||||
# Exclude partial split outputs or leftovers from failed builds. Moving
|
||||
# them out of the flat publication directory keeps them available for
|
||||
# diagnosis without handing them to sign/promote.
|
||||
unpublished=""
|
||||
for path in "$BUILD_OUTPUT_DIR"/*.pkg.tar.*; do
|
||||
[[ -f "$path" ]] || continue
|
||||
file=${path##*/}
|
||||
if ! grep -Fxq -- "${file%.sig}" "$BUILD_RESULT_DIR/artifacts"; then
|
||||
[[ -n "$unpublished" ]] || unpublished=$(mktemp -d "$BUILD_OUTPUT_DIR/.unpublished.XXXXXX")
|
||||
mv -- "$path" "$unpublished/"
|
||||
fi
|
||||
done
|
||||
print_warning "Some packages failed; publishing the completed packages before retrying the failures"
|
||||
elif [[ "$build_status" != 0 ]]; then
|
||||
print_error "Build failed"
|
||||
notify_error "Release failed: Build step failed" "$RELEASE_CONTEXT"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo ""
|
||||
@@ -151,6 +187,12 @@ BUILT_COUNT=$(grep -c '' <<<"$BUILT_FILES")
|
||||
[[ -z "$BUILT_FILES" ]] && BUILT_COUNT=0
|
||||
print_info "Built $BUILT_COUNT package(s) this run"
|
||||
|
||||
if [[ "$partial" == true && "$BUILT_COUNT" == 0 ]]; then
|
||||
print_error "No completed packages to publish"
|
||||
notify_error "Release failed: No completed packages" "$RELEASE_CONTEXT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 2: Sign
|
||||
echo ""
|
||||
print_info "Step 2/6: Signing packages..."
|
||||
@@ -209,7 +251,16 @@ if ((BUILT_COUNT > 0)); then
|
||||
summary+="<br><br><strong>$BUILT_COUNT package(s) published:</strong>"
|
||||
summary+="$(format_package_list_html "$BUILT_FILES")"
|
||||
summary+="<br><br>Live at https://pkgs.omarchy.org/$MIRROR/$ARCH/"
|
||||
notify_success "Release published: $MIRROR" "$summary"
|
||||
if [[ "$partial" == true ]]; then
|
||||
failed=$(tr '\n' ' ' < "$BUILD_RESULT_DIR/failed" | basecamp_html_escape)
|
||||
blocked=$(tr '\n' ' ' < "$BUILD_RESULT_DIR/blocked" | basecamp_html_escape)
|
||||
[[ -z "$failed" ]] || summary+="<br><br>Failed: $failed"
|
||||
[[ -z "$blocked" ]] || summary+="<br>Blocked by failed dependencies: $blocked"
|
||||
summary+="<br>Published packages will be skipped on retry; failed packages remain queued."
|
||||
notify_info "Partial release published: $MIRROR" "$summary"
|
||||
else
|
||||
notify_success "Release published: $MIRROR" "$summary"
|
||||
fi
|
||||
else
|
||||
# Rare by construction: a release only runs when the version check queued
|
||||
# work, so publishing nothing means the check and the builder disagreed
|
||||
@@ -224,4 +275,10 @@ else
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$partial" == true ]]; then
|
||||
print_warning "Completed packages published; unsuccessful packages remain for retry"
|
||||
# Preserve the scheduled queue and failure backoff. The next version
|
||||
# check/build compares against the updated repository and skips successes.
|
||||
exit 1
|
||||
fi
|
||||
print_success "Release workflow completed successfully!"
|
||||
+15
-10
@@ -62,8 +62,8 @@ if [[ ! -d "$REPO_DIR" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Docker is available
|
||||
check_docker
|
||||
# Check the selected container engine is available
|
||||
check_engine
|
||||
|
||||
# Find package files to confirm before running Docker
|
||||
cd "$REPO_DIR"
|
||||
@@ -88,24 +88,29 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build/update the Docker image (always use x86_64 for removal - it's architecture independent)
|
||||
# repo-remove is mirror-independent — always use the edge x86_64 image
|
||||
build_docker_image "$BUILD_DIR" "x86_64" "edge"
|
||||
# repo-remove is architecture-independent, so use a host-native edge image.
|
||||
TOOL_ARCH=$(docker_native_arch) || {
|
||||
print_error "Unsupported host architecture: $(uname -m)"
|
||||
exit 1
|
||||
}
|
||||
build_docker_image "$BUILD_DIR" "$TOOL_ARCH" "edge"
|
||||
|
||||
acquire_release_lock || exit 1
|
||||
|
||||
print_info "Removing package..."
|
||||
|
||||
# Ensure directory is writable by container user
|
||||
make_dir_writable "$REPO_DIR"
|
||||
# Rootless Podman uses keep-id and leaves host ownership/modes intact.
|
||||
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||
make_dir_writable "$REPO_DIR"
|
||||
fi
|
||||
|
||||
# Run the removal script in Docker (always use x86_64 image)
|
||||
docker run --rm --platform linux/amd64 \
|
||||
# Run the removal script in the host-native image.
|
||||
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$TOOL_ARCH")" \
|
||||
-e ARCH="$ARCH" \
|
||||
-e MIRROR="$MIRROR" \
|
||||
-v "$REPO_ROOT:/pkgs.omarchy.org" \
|
||||
-v "$BUILD_DIR:/build:ro" \
|
||||
omarchy-pkg-builder:latest-x86_64-edge /build/remove-package.sh "$PACKAGE_NAME"
|
||||
"omarchy-pkg-builder:latest-$TOOL_ARCH-edge" /build/remove-package.sh "$PACKAGE_NAME"
|
||||
|
||||
RESULT=$?
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ set -e
|
||||
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/docker-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
|
||||
CHECK_ONLY=false
|
||||
SKIP_TIMERS=false
|
||||
@@ -61,10 +63,12 @@ if command -v apt-get >/dev/null 2>&1; then
|
||||
DISTRO="debian"
|
||||
PKG_BSDTAR="libarchive-tools"
|
||||
PKG_DOCKER="docker.io"
|
||||
PKG_PODMAN="podman"
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
DISTRO="arch"
|
||||
PKG_BSDTAR="libarchive"
|
||||
PKG_DOCKER="docker"
|
||||
PKG_PODMAN="podman"
|
||||
else
|
||||
print_error "Unsupported distribution — need apt-get or pacman"
|
||||
exit 1
|
||||
@@ -72,18 +76,36 @@ fi
|
||||
|
||||
print_info "Distribution: $DISTRO"
|
||||
|
||||
# A fresh repository host still defaults to Docker. An explicit Podman choice,
|
||||
# or a working Podman selected by the shared helper, is left alone.
|
||||
if [[ -z "$CONTAINER_ENGINE" ]]; then
|
||||
CONTAINER_ENGINE=docker
|
||||
export CONTAINER_ENGINE
|
||||
fi
|
||||
if ! container_engine_supported; then
|
||||
print_error "Unsupported CONTAINER_ENGINE: $CONTAINER_ENGINE (use docker or podman)"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||
PKG_ENGINE="$PKG_DOCKER"
|
||||
ENGINE_NAME="Docker"
|
||||
else
|
||||
PKG_ENGINE="$PKG_PODMAN"
|
||||
ENGINE_NAME="Podman"
|
||||
fi
|
||||
|
||||
if [[ "$CHECK_ONLY" != true && $EUID -ne 0 ]]; then
|
||||
print_error "Run as root (installing packages and systemd units)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Docker and the release timers are both systemd units. Say so plainly rather
|
||||
# than failing later on a missing command — a container is the usual way to end
|
||||
# up here, and it cannot be a repository host.
|
||||
# The release timers are systemd units. Say so plainly rather than failing
|
||||
# later on a missing command — a container is the usual way to end up here,
|
||||
# and it cannot be a repository host.
|
||||
if [[ "$CHECK_ONLY" != true ]] && ! command -v systemctl >/dev/null 2>&1; then
|
||||
print_error "systemctl not found — the repository host must run systemd"
|
||||
echo ""
|
||||
echo "Docker and the release timers are systemd units. This looks like a"
|
||||
echo "The release timers are systemd units. This looks like a"
|
||||
echo "container; run setup on the host itself."
|
||||
exit 1
|
||||
fi
|
||||
@@ -139,24 +161,27 @@ else
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# --- docker ------------------------------------------------------------------
|
||||
# --- container engine --------------------------------------------------------
|
||||
|
||||
# Docker is left alone when it already works. A host may well be running a
|
||||
# The selected engine is left alone when it already works. A host may be running a
|
||||
# version from Docker's own repository rather than the distribution's, and
|
||||
# replacing that underneath a working builder would be a poor trade for
|
||||
# tidiness.
|
||||
print_info "Checking Docker..."
|
||||
print_info "Checking $ENGINE_NAME..."
|
||||
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
print_step "docker present: $(docker --version 2>/dev/null | head -1)"
|
||||
if docker info >/dev/null 2>&1; then
|
||||
print_success "Docker is installed and running — leaving it alone"
|
||||
if command -v "$CONTAINER_ENGINE" >/dev/null 2>&1; then
|
||||
print_step "$CONTAINER_ENGINE present: $("$CONTAINER_ENGINE" --version 2>/dev/null | head -1)"
|
||||
if "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
|
||||
print_success "$ENGINE_NAME is installed and available — leaving it alone"
|
||||
elif [[ "$CHECK_ONLY" == true ]]; then
|
||||
print_warning "Docker is installed but not running; would start it"
|
||||
print_warning "$ENGINE_NAME is installed but unavailable"
|
||||
elif [[ "$CONTAINER_ENGINE" == "podman" ]]; then
|
||||
print_error "Podman is installed but unavailable to the current user"
|
||||
exit 1
|
||||
else
|
||||
print_info "Docker is installed but not running — starting it"
|
||||
systemctl enable --now docker.service
|
||||
if docker info >/dev/null 2>&1; then
|
||||
if "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
|
||||
print_success "Docker started"
|
||||
else
|
||||
print_error "Docker is installed but still not responding"
|
||||
@@ -165,16 +190,24 @@ if command -v docker >/dev/null 2>&1; then
|
||||
fi
|
||||
fi
|
||||
elif [[ "$CHECK_ONLY" == true ]]; then
|
||||
print_warning "Would install $PKG_DOCKER and enable it"
|
||||
else
|
||||
print_info "Installing $PKG_DOCKER..."
|
||||
install_packages "$PKG_DOCKER"
|
||||
systemctl enable --now docker.service
|
||||
if docker info >/dev/null 2>&1; then
|
||||
print_success "Docker installed and running"
|
||||
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||
print_warning "Would install $PKG_ENGINE and enable it"
|
||||
else
|
||||
print_error "Docker installed but not responding"
|
||||
echo " Check 'systemctl status docker' — builds cannot run without it."
|
||||
print_warning "Would install $PKG_ENGINE"
|
||||
fi
|
||||
else
|
||||
print_info "Installing $PKG_ENGINE..."
|
||||
install_packages "$PKG_ENGINE"
|
||||
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||
systemctl enable --now docker.service
|
||||
fi
|
||||
if "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
|
||||
print_success "$ENGINE_NAME installed and available"
|
||||
else
|
||||
print_error "$ENGINE_NAME installed but not responding"
|
||||
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||
echo " Check 'systemctl status docker' — builds cannot run without it."
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -237,6 +270,10 @@ echo ""
|
||||
|
||||
# --- release timers ----------------------------------------------------------
|
||||
|
||||
print_info "Published architectures: $(published_arches | tr '\n' ' ')"
|
||||
echo " (PUBLISHED_ARCHES in helpers/paths.sh; OMARCHY_ARCHES overrides a one-off command)"
|
||||
echo ""
|
||||
|
||||
TIMERS=(omarchy-check-versions omarchy-auto-release-edge omarchy-auto-release-rc omarchy-auto-release-stable)
|
||||
|
||||
if [[ "$SKIP_TIMERS" == true ]]; then
|
||||
|
||||
@@ -52,8 +52,8 @@ if [[ ! -d "$BUILD_OUTPUT_DIR" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Docker is available
|
||||
check_docker
|
||||
# Check the selected container engine is available
|
||||
check_engine
|
||||
|
||||
# Check GPG credentials are in environment
|
||||
if [[ -z "$GPG_PRIVATE_KEY" ]]; then
|
||||
@@ -66,23 +66,30 @@ if [[ -z "$GPG_PASSPHRASE" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build/update the Docker image (always use x86_64 for signing - it's architecture independent)
|
||||
build_docker_image "$BUILD_DIR" "x86_64" "$MIRROR"
|
||||
# Signing is architecture-independent, so run its utility container natively
|
||||
# on either an x86_64 or ARM host.
|
||||
TOOL_ARCH=$(docker_native_arch) || {
|
||||
print_error "Unsupported host architecture: $(uname -m)"
|
||||
exit 1
|
||||
}
|
||||
build_docker_image "$BUILD_DIR" "$TOOL_ARCH" "$MIRROR"
|
||||
|
||||
print_info "Running package signing..."
|
||||
|
||||
# Ensure output directory is writable by container user
|
||||
make_dir_writable "$BUILD_OUTPUT_DIR"
|
||||
# Rootless Podman uses keep-id and leaves host ownership/modes intact.
|
||||
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||
make_dir_writable "$BUILD_OUTPUT_DIR"
|
||||
fi
|
||||
|
||||
# Run the signing script in Docker (always use x86_64 image)
|
||||
docker run --rm --platform linux/amd64 \
|
||||
# Run the signing script in the host-native image.
|
||||
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$TOOL_ARCH")" \
|
||||
-e ARCH="$ARCH" \
|
||||
-e MIRROR="$MIRROR" \
|
||||
-e GPG_PRIVATE_KEY="$GPG_PRIVATE_KEY" \
|
||||
-e GPG_PASSPHRASE="$GPG_PASSPHRASE" \
|
||||
-v "$BUILD_ROOT/build-output:/build-output" \
|
||||
-v "$BUILD_DIR:/build:ro" \
|
||||
omarchy-pkg-builder:latest-x86_64-$MIRROR /build/sign.sh
|
||||
"omarchy-pkg-builder:latest-$TOOL_ARCH-$MIRROR" /build/sign.sh
|
||||
|
||||
SIGN_RESULT=$?
|
||||
|
||||
|
||||
-434
@@ -1,434 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
source "$BUILD_ROOT/helpers/package-metadata.sh"
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
|
||||
SPECIFIC_PACKAGES=()
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [PACKAGE...]
|
||||
|
||||
Sync AUR-backed packages into pkgbuilds/<package>/.
|
||||
|
||||
Package selection is driven by pkgbuilds/<package>/.omarchy/package.json:
|
||||
{ "source": "aur" } # synced from matching AUR package name
|
||||
{ "source": "aur", "aur": "yaru" } # synced from different AUR package name
|
||||
{ "source": "aur", "sync": false } # AUR-origin, but not auto-synced
|
||||
|
||||
Arguments:
|
||||
PACKAGE One or more package names to sync (optional)
|
||||
|
||||
Examples:
|
||||
$0 # Sync all AUR packages with sync enabled
|
||||
$0 yay cursor-bin # Sync specific packages
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--tier)
|
||||
print_error "--tier is no longer supported; package metadata controls sync behavior"
|
||||
exit 1
|
||||
;;
|
||||
--*)
|
||||
print_error "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
SPECIFIC_PACKAGES+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
print_header "AUR Package Sync"
|
||||
mkdir -p "$PKGBUILDS_DIR"
|
||||
|
||||
SYNCED=0
|
||||
SKIPPED=0
|
||||
FAILED=0
|
||||
SYNCED_PACKAGES=()
|
||||
SPECIFIC_MODE=false
|
||||
|
||||
get_pkgbuild_field() {
|
||||
local package_dir="$1"
|
||||
local field="$2"
|
||||
local value=""
|
||||
|
||||
if [[ -f "$package_dir/PKGBUILD" ]]; then
|
||||
value=$(grep -m1 "^${field}=" "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'") || true
|
||||
if [[ -n "$value" ]]; then
|
||||
echo "$value"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f "$package_dir/.SRCINFO" ]]; then
|
||||
awk -F' = ' -v field="$field" '$1 ~ "^[[:space:]]*" field "$" { print $2; exit }' "$package_dir/.SRCINFO"
|
||||
fi
|
||||
}
|
||||
|
||||
set_pkgrel() {
|
||||
local package_dir="$1"
|
||||
local pkgrel="$2"
|
||||
local pkgbuild="$package_dir/PKGBUILD"
|
||||
|
||||
if [[ -f "$pkgbuild" ]]; then
|
||||
sed -i "s/^pkgrel=.*/pkgrel=$pkgrel/" "$pkgbuild"
|
||||
fi
|
||||
}
|
||||
|
||||
display_package_name() {
|
||||
local package_dir="$1"
|
||||
local name
|
||||
name=$(basename "$package_dir")
|
||||
echo "${name%.work}"
|
||||
}
|
||||
|
||||
remove_aur_only_files() {
|
||||
local package_dir="$1"
|
||||
|
||||
rm -f "$package_dir/.SRCINFO" "$package_dir/.gitignore"
|
||||
}
|
||||
|
||||
copy_aur_contents() {
|
||||
local aur_dir="$1"
|
||||
local target_dir="$2"
|
||||
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
shopt -s dotglob nullglob
|
||||
local item base
|
||||
for item in "$aur_dir"/*; do
|
||||
base=$(basename "$item")
|
||||
[[ "$base" == ".git" ]] && continue
|
||||
cp -a "$item" "$target_dir/"
|
||||
done
|
||||
shopt -u dotglob nullglob
|
||||
}
|
||||
|
||||
commit_synced_worktree() {
|
||||
local work_dir="$1"
|
||||
local target_dir="$2"
|
||||
local parent base staged_dir backup_root backup_dir
|
||||
|
||||
parent=$(dirname "$target_dir")
|
||||
base=$(basename "$target_dir")
|
||||
staged_dir=$(mktemp -d "$parent/.${base}.staged.XXXXXX")
|
||||
backup_root=$(mktemp -d "$parent/.${base}.backup.XXXXXX")
|
||||
backup_dir="$backup_root/$base"
|
||||
|
||||
# Copy to the package filesystem before swapping. This keeps the original
|
||||
# package directory intact if copying from /tmp fails or is interrupted.
|
||||
if ! cp -a "$work_dir/." "$staged_dir/"; then
|
||||
print_error "Failed to stage synced package for $base"
|
||||
rm -rf "$staged_dir" "$backup_root"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -e "$target_dir" ]]; then
|
||||
if ! mv "$target_dir" "$backup_dir"; then
|
||||
print_error "Failed to back up existing package directory: $target_dir"
|
||||
rm -rf "$staged_dir" "$backup_root"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! mv "$staged_dir" "$target_dir"; then
|
||||
print_error "Failed to install synced package directory: $target_dir"
|
||||
if [[ -e "$backup_dir" ]]; then
|
||||
mv "$backup_dir" "$target_dir" || true
|
||||
fi
|
||||
rm -rf "$staged_dir" "$backup_root"
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -rf "$backup_root" "$work_dir"
|
||||
}
|
||||
|
||||
set_upstream_commit() {
|
||||
local package_dir="$1"
|
||||
local commit="$2"
|
||||
local metadata="$package_dir/.omarchy/package.json"
|
||||
local tmpfile
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
jq --arg commit "$commit" '.upstream_commit = $commit' "$metadata" > "$tmpfile"
|
||||
mv "$tmpfile" "$metadata"
|
||||
}
|
||||
|
||||
apply_omarchy_patches() {
|
||||
local package_dir="$1"
|
||||
local patches_dir="$package_dir/.omarchy/patches"
|
||||
local applied=false
|
||||
|
||||
[[ -d "$patches_dir" ]] || return 1
|
||||
|
||||
shopt -s nullglob
|
||||
local patch_files=("$patches_dir"/*.patch)
|
||||
local patch_dir_files=("$patches_dir"/*)
|
||||
shopt -u nullglob
|
||||
|
||||
if [[ ${#patch_files[@]} -eq 0 ]]; then
|
||||
if [[ ${#patch_dir_files[@]} -gt 0 ]]; then
|
||||
print_warning "No .patch files found in $patches_dir"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_info "Applying Omarchy patches for $(display_package_name "$package_dir")..."
|
||||
local patch_file
|
||||
for patch_file in "${patch_files[@]}"; do
|
||||
print_info " $(basename "$patch_file")"
|
||||
if ! (cd "$package_dir" && patch -p1 --forward --batch --no-backup-if-mismatch < "$patch_file"); then
|
||||
print_error "Failed to apply patch: $patch_file"
|
||||
return 2
|
||||
fi
|
||||
applied=true
|
||||
done
|
||||
|
||||
[[ "$applied" == true ]]
|
||||
}
|
||||
|
||||
run_omarchy_post_sync_hook() {
|
||||
local package_dir="$1"
|
||||
local package="$2"
|
||||
local aur_package="$3"
|
||||
local aur_pkgrel="$4"
|
||||
local hook="$package_dir/.omarchy/post-sync.sh"
|
||||
|
||||
[[ -f "$hook" ]] || return 1
|
||||
|
||||
print_info "Running Omarchy post-sync hook for $(display_package_name "$package_dir")..."
|
||||
if ! (
|
||||
cd "$package_dir"
|
||||
PACKAGE_NAME="$package" \
|
||||
AUR_PACKAGE_NAME="$aur_package" \
|
||||
AUR_PKGREL="$aur_pkgrel" \
|
||||
bash ".omarchy/post-sync.sh"
|
||||
); then
|
||||
print_error "Failed to run post-sync hook: $hook"
|
||||
return 2
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
apply_pkgrel_suffix_if_customized() {
|
||||
local package_dir="$1"
|
||||
local aur_pkgrel="$2"
|
||||
|
||||
[[ -n "$aur_pkgrel" ]] || return 0
|
||||
|
||||
print_info "Applying Omarchy pkgrel suffix for $(display_package_name "$package_dir"): pkgrel=$aur_pkgrel.1"
|
||||
set_pkgrel "$package_dir" "$aur_pkgrel.1"
|
||||
}
|
||||
|
||||
apply_pkgrel_override() {
|
||||
local package_dir="$1"
|
||||
local aur_pkgrel="$2"
|
||||
local previous_pkgver="$3"
|
||||
local metadata="$package_dir/.omarchy/package.json"
|
||||
local pkgbuild="$package_dir/PKGBUILD"
|
||||
|
||||
[[ -f "$metadata" ]] || return 1
|
||||
jq -e 'has("pkgrel")' "$metadata" >/dev/null || return 1
|
||||
|
||||
local current_pkgver suffix offset base rel tmpfile
|
||||
# Read through the same accessor that produced previous_pkgver. Parsing it a
|
||||
# second time here let a quoted pkgver= compare unequal to itself, which threw
|
||||
# away the pkgrel metadata of an unchanged package on every sync.
|
||||
current_pkgver=$(get_pkgbuild_field "$package_dir" pkgver)
|
||||
|
||||
if [[ -n "$previous_pkgver" && "$current_pkgver" != "$previous_pkgver" ]]; then
|
||||
print_info "Removing stale pkgrel metadata for $(display_package_name "$package_dir") (pkgver changed: $previous_pkgver -> $current_pkgver)"
|
||||
tmpfile=$(mktemp)
|
||||
jq 'del(.pkgrel)' "$metadata" > "$tmpfile"
|
||||
mv "$tmpfile" "$metadata"
|
||||
return 1
|
||||
fi
|
||||
|
||||
suffix=$(jq -r '.pkgrel.suffix // 1' "$metadata")
|
||||
offset=$(jq -r '.pkgrel.offset // 0' "$metadata")
|
||||
|
||||
if [[ ! "$offset" =~ ^[0-9]+$ || ! "$aur_pkgrel" =~ ^[0-9]+$ ]]; then
|
||||
print_error "pkgrel offset requires numeric AUR pkgrel for $(display_package_name "$package_dir")"
|
||||
return 2
|
||||
fi
|
||||
|
||||
base=$((aur_pkgrel + offset))
|
||||
rel="$base.$suffix"
|
||||
|
||||
print_info "Applying pkgrel suffix for $(display_package_name "$package_dir"): AUR pkgrel=$aur_pkgrel, offset=$offset, suffix=$suffix -> pkgrel=$rel"
|
||||
set_pkgrel "$package_dir" "$rel"
|
||||
return 0
|
||||
}
|
||||
|
||||
clone_aur_package() {
|
||||
local aur_package="$1"
|
||||
local dest="$2"
|
||||
local clone_log="$TEMP_DIR/clone.log"
|
||||
local attempt
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
rm -rf "$dest"
|
||||
if git clone "https://aur.archlinux.org/${aur_package}.git" "$dest" >"$clone_log" 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
if [[ $attempt -lt 3 ]]; then
|
||||
print_warning "Clone of $aur_package failed (attempt $attempt/3), retrying in 10s..."
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
|
||||
print_warning "Failed to clone $aur_package after 3 attempts: $(tail -n 1 "$clone_log")"
|
||||
return 1
|
||||
}
|
||||
|
||||
sync_package() {
|
||||
local package="$1"
|
||||
local package_dir="$PKGBUILDS_DIR/$package"
|
||||
local metadata="$package_dir/.omarchy/package.json"
|
||||
|
||||
if [[ ! -f "$metadata" ]]; then
|
||||
if [[ "$SPECIFIC_MODE" == true ]]; then
|
||||
print_error "Package $package is missing .omarchy/package.json"
|
||||
((++FAILED))
|
||||
else
|
||||
print_warning "Skipping $package: missing .omarchy/package.json"
|
||||
((++SKIPPED))
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$(jq -r '.source // ""' "$metadata")" != "aur" ]]; then
|
||||
print_info "Skipping $package: source is not AUR"
|
||||
((++SKIPPED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$(jq -r 'if has("sync") then .sync else true end' "$metadata")" == "false" ]]; then
|
||||
print_info "Skipping $package: AUR sync disabled"
|
||||
((++SKIPPED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
local aur_package
|
||||
aur_package=$(jq -r --arg package "$package" '.aur // $package' "$metadata")
|
||||
|
||||
if [[ "$aur_package" == "$package" ]]; then
|
||||
print_info "Syncing $package from AUR..."
|
||||
else
|
||||
print_info "Syncing $package from AUR package $aur_package..."
|
||||
fi
|
||||
|
||||
cd "$TEMP_DIR"
|
||||
|
||||
if ! clone_aur_package "$aur_package" "$TEMP_DIR/$aur_package"; then
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$TEMP_DIR/$aur_package/PKGBUILD" ]]; then
|
||||
print_warning "AUR repository for $aur_package is empty (package does not exist in AUR)"
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
local aur_dir="$TEMP_DIR/$aur_package"
|
||||
local work_dir="$TEMP_DIR/${package}.work"
|
||||
local aur_pkgrel previous_pkgver upstream_commit
|
||||
aur_pkgrel=$(get_pkgbuild_field "$aur_dir" pkgrel)
|
||||
previous_pkgver=$(get_pkgbuild_field "$package_dir" pkgver || true)
|
||||
upstream_commit=$(git -C "$aur_dir" rev-parse HEAD)
|
||||
|
||||
rm -rf "$work_dir"
|
||||
copy_aur_contents "$aur_dir" "$work_dir"
|
||||
rm -rf "$work_dir/.omarchy"
|
||||
cp -a "$package_dir/.omarchy" "$work_dir/.omarchy"
|
||||
|
||||
local customized=false
|
||||
|
||||
if apply_omarchy_patches "$work_dir"; then
|
||||
customized=true
|
||||
else
|
||||
local patch_status=$?
|
||||
if [[ $patch_status -eq 2 ]]; then
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if run_omarchy_post_sync_hook "$work_dir" "$package" "$aur_package" "$aur_pkgrel"; then
|
||||
customized=true
|
||||
else
|
||||
local hook_status=$?
|
||||
if [[ $hook_status -eq 2 ]]; then
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
local pkgrel_overridden=false
|
||||
set +e
|
||||
apply_pkgrel_override "$work_dir" "$aur_pkgrel" "$previous_pkgver"
|
||||
local pkgrel_status=$?
|
||||
set -e
|
||||
case "$pkgrel_status" in
|
||||
0) pkgrel_overridden=true ;;
|
||||
1) ;;
|
||||
*) ((++FAILED)); return 0 ;;
|
||||
esac
|
||||
|
||||
if [[ "$customized" == true && "$pkgrel_overridden" == false ]]; then
|
||||
apply_pkgrel_suffix_if_customized "$work_dir" "$aur_pkgrel"
|
||||
fi
|
||||
|
||||
remove_aur_only_files "$work_dir"
|
||||
|
||||
set_upstream_commit "$work_dir" "$upstream_commit"
|
||||
if ! commit_synced_worktree "$work_dir" "$package_dir"; then
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
SYNCED_PACKAGES+=("$package")
|
||||
((++SYNCED))
|
||||
}
|
||||
|
||||
if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 ]]; then
|
||||
SPECIFIC_MODE=true
|
||||
for package in "${SPECIFIC_PACKAGES[@]}"; do
|
||||
sync_package "$package"
|
||||
done
|
||||
else
|
||||
while IFS= read -r package; do
|
||||
sync_package "$package"
|
||||
done < <(packages_for_aur_sync)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
print_error "Sync completed with failures"
|
||||
else
|
||||
print_success "Sync complete!"
|
||||
fi
|
||||
echo " Target: $PKGBUILDS_DIR"
|
||||
echo " Synced: $SYNCED"
|
||||
echo " Skipped: $SKIPPED"
|
||||
echo " Failed: $FAILED"
|
||||
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
+416
-75
@@ -17,11 +17,14 @@ SELF_TEST=false
|
||||
# rebuilding for it would ship a package built against the wrong ABI.
|
||||
OFFICIAL_REPOS=" core extra multilib core-debug extra-debug "
|
||||
|
||||
# The published repository, used as the floor a bumped pkgrel has to clear.
|
||||
# Only x86_64 is published today; aarch64 has no repository to compare against.
|
||||
# The published repositories are the floor a bumped pkgrel has to clear.
|
||||
PUBLISHED_BASE_URL="${OMARCHY_PUBLISHED_BASE_URL:-https://pkgs.omarchy.org}"
|
||||
PUBLISHED_MIRRORS=(edge stable)
|
||||
PUBLISHED_ARCH=x86_64
|
||||
|
||||
# Arch Linux ARM has no dated snapshots. This is the same live repository the
|
||||
# aarch64 builder resolves; override it only when the builder mirror changes.
|
||||
ALARM_BASE_URL="${OMARCHY_ALARM_BASE_URL:-https://fl.us.mirror.archlinuxarm.org/aarch64}"
|
||||
ALARM_REPOS=(core extra alarm aur)
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
@@ -34,10 +37,13 @@ A package opts in by naming those dependencies in .omarchy/package.json:
|
||||
|
||||
{ "source": "aur", "sync": false, "rebuild_on": ["qt6-base"] }
|
||||
|
||||
The versions the current pkgrel was bumped for are recorded alongside, in
|
||||
rebuilt_against, and written by this command:
|
||||
The versions the current pkgrel was bumped for are recorded per published
|
||||
architecture in rebuilt_against, and written by this command:
|
||||
|
||||
{ "rebuild_on": ["qt6-base"], "rebuilt_against": { "qt6-base": "6.11.2-2" } }
|
||||
{ "rebuild_on": ["qt6-base"], "rebuilt_against": {
|
||||
"x86_64": { "qt6-base": "6.11.2-3" },
|
||||
"aarch64": { "qt6-base": "6.11.2-2" }
|
||||
} }
|
||||
|
||||
pkgrel is bumped unless every package named in rebuild_on is recorded and still
|
||||
matches. A name that is missing from the record counts as changed, so opting a
|
||||
@@ -60,8 +66,11 @@ Examples:
|
||||
$0 # Update every package that declares rebuild_on
|
||||
$0 quickshell-git # Update specific packages
|
||||
|
||||
Trigger versions are read from the local pacman database, so sync it first
|
||||
(pacman -Sy) or this reports whatever that database last saw.
|
||||
x86_64 trigger versions come from the local pacman database; aarch64 versions
|
||||
come from the live Arch Linux ARM repository database. A trigger this
|
||||
repository carries for an architecture shadows both, so its version is the
|
||||
checked-in recipe's once edge publishes it. Every architecture in CI_ARCHES
|
||||
(what a merge builds) that the package supports is considered.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -94,7 +103,7 @@ SPECIFIC_MODE=false
|
||||
# The version of a trigger package as the build container would resolve it.
|
||||
# A name pacman does not know reports nothing rather than failing, so the caller
|
||||
# gets to say which package was left alone instead of the run dying here.
|
||||
repo_version() {
|
||||
native_repo_version() {
|
||||
local package="$1"
|
||||
local info
|
||||
|
||||
@@ -108,9 +117,61 @@ repo_version() {
|
||||
' <<<"$info"
|
||||
}
|
||||
|
||||
load_alarm_repo() {
|
||||
local repo="$1" db index
|
||||
index="$TEMP_DIR/alarm-$repo.index"
|
||||
[[ -f "$index" ]] && return 0
|
||||
|
||||
db="$TEMP_DIR/alarm-$repo.db"
|
||||
if ! curl -fsSL --max-time 120 -o "$db" "$ALARM_BASE_URL/$repo/$repo.db" 2>/dev/null; then
|
||||
print_error "Could not read the aarch64 $repo repository database"
|
||||
return 1
|
||||
fi
|
||||
if ! tar -tf "$db" >/dev/null 2>&1; then
|
||||
print_error "Unreadable aarch64 $repo repository database"
|
||||
return 1
|
||||
fi
|
||||
|
||||
tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk '
|
||||
function emit() {
|
||||
if (name != "" && version != "") {
|
||||
print name "\t" version
|
||||
if (base != "" && base != name) print base "\t" version
|
||||
}
|
||||
name=""; base=""; version=""
|
||||
}
|
||||
$0 == "%FILENAME%" { emit(); next }
|
||||
$0 == "%NAME%" { if (name != "" && version != "") emit(); getline; name=$0; next }
|
||||
$0 == "%BASE%" { getline; base=$0; next }
|
||||
$0 == "%VERSION%" { getline; version=$0; next }
|
||||
END { emit() }
|
||||
' >"$index"
|
||||
}
|
||||
|
||||
alarm_repo_version() {
|
||||
local package="$1" repo version
|
||||
for repo in "${ALARM_REPOS[@]}"; do
|
||||
load_alarm_repo "$repo" || return 1
|
||||
version=$(awk -F '\t' -v package="$package" '$1 == package { print $2; exit }' "$TEMP_DIR/alarm-$repo.index")
|
||||
if [[ -n "$version" ]]; then
|
||||
echo "$version"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
repo_version() { # repo_version <arch> <package>
|
||||
case "$1" in
|
||||
x86_64) native_repo_version "$2" ;;
|
||||
aarch64) alarm_repo_version "$2" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
declare -A PUBLISHED_VERSION=()
|
||||
declare -A EDGE_VERSION=()
|
||||
declare -A EDGE_READ=()
|
||||
PUBLISHED_LOADED=false
|
||||
PUBLISHED_AVAILABLE=true
|
||||
|
||||
remember_published() {
|
||||
local name="$1"
|
||||
@@ -131,34 +192,44 @@ load_published_versions() {
|
||||
[[ "$PUBLISHED_LOADED" == true ]] && return 0
|
||||
PUBLISHED_LOADED=true
|
||||
|
||||
local mirror db name base version
|
||||
local arch mirror db name base version
|
||||
|
||||
for mirror in "${PUBLISHED_MIRRORS[@]}"; do
|
||||
db="$TEMP_DIR/published-$mirror.db.tar.zst"
|
||||
for arch in $(ci_arches); do
|
||||
for mirror in "${PUBLISHED_MIRRORS[@]}"; do
|
||||
db="$TEMP_DIR/published-$mirror-$arch.db.tar.zst"
|
||||
|
||||
if ! curl -fsSL --max-time 120 -o "$db" \
|
||||
"$PUBLISHED_BASE_URL/$mirror/$PUBLISHED_ARCH/omarchy.db.tar.zst" 2>/dev/null; then
|
||||
print_warning "Could not read the published $mirror database; bumps are not checked against it this run"
|
||||
PUBLISHED_AVAILABLE=false
|
||||
continue
|
||||
fi
|
||||
if ! curl -fsSL --max-time 120 -o "$db" \
|
||||
"$PUBLISHED_BASE_URL/$mirror/$arch/omarchy.db.tar.zst" 2>/dev/null; then
|
||||
print_warning "Could not read the published $mirror/$arch database; bumps are not checked against it this run"
|
||||
continue
|
||||
fi
|
||||
if ! tar -tf "$db" >/dev/null 2>&1; then
|
||||
print_warning "Unreadable published $mirror/$arch database; bumps are not checked against it this run"
|
||||
continue
|
||||
fi
|
||||
[[ "$mirror" == edge ]] && EDGE_READ["$arch"]=1
|
||||
|
||||
while IFS=$'\t' read -r name base version; do
|
||||
[[ -n "$name" && -n "$version" ]] && remember_published "$name" "$version"
|
||||
[[ -n "$base" && -n "$version" ]] && remember_published "$base" "$version"
|
||||
done < <(
|
||||
tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk '
|
||||
function emit() {
|
||||
if (name != "" && version != "") print name "\t" base "\t" version
|
||||
name=""; base=""; version=""
|
||||
}
|
||||
$0 == "%FILENAME%" { emit(); next }
|
||||
$0 == "%NAME%" { if (name != "" && version != "") emit(); getline; name=$0; next }
|
||||
$0 == "%BASE%" { getline; base=$0; next }
|
||||
$0 == "%VERSION%" { getline; version=$0; next }
|
||||
END { emit() }
|
||||
'
|
||||
)
|
||||
while IFS=$'\t' read -r name base version; do
|
||||
[[ -n "$name" && -n "$version" ]] && remember_published "$name" "$version"
|
||||
[[ -n "$base" && -n "$version" ]] && remember_published "$base" "$version"
|
||||
if [[ "$mirror" == edge && -n "$version" ]]; then
|
||||
[[ -z "$name" ]] || EDGE_VERSION["$arch/$name"]="$version"
|
||||
[[ -z "$base" ]] || EDGE_VERSION["$arch/$base"]="$version"
|
||||
fi
|
||||
done < <(
|
||||
tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk '
|
||||
function emit() {
|
||||
if (name != "" && version != "") print name "\t" base "\t" version
|
||||
name=""; base=""; version=""
|
||||
}
|
||||
$0 == "%FILENAME%" { emit(); next }
|
||||
$0 == "%NAME%" { if (name != "" && version != "") emit(); getline; name=$0; next }
|
||||
$0 == "%BASE%" { getline; base=$0; next }
|
||||
$0 == "%VERSION%" { getline; version=$0; next }
|
||||
END { emit() }
|
||||
'
|
||||
)
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
@@ -169,6 +240,29 @@ published_version() {
|
||||
echo "${PUBLISHED_VERSION[$package]:-}"
|
||||
}
|
||||
|
||||
# A trigger this repository builds for the architecture. The builder lists
|
||||
# [omarchy] ahead of the distribution repositories, so this recipe, not Arch or
|
||||
# Arch Linux ARM, decides what a dependent links against.
|
||||
carried_trigger_dir() { # carried_trigger_dir <arch> <trigger>
|
||||
local pkgdir
|
||||
pkgdir=$(package_dir_for_name "$2") || return 1
|
||||
package_has_metadata "$pkgdir" || return 1
|
||||
package_builds_for_mirror "$pkgdir" edge || return 1
|
||||
package_supports_arch "$pkgdir" "$1" || return 1
|
||||
echo "$pkgdir"
|
||||
}
|
||||
|
||||
carried_version() { # carried_version <arch> <pkgdir>
|
||||
local epoch pkgver pkgrel
|
||||
epoch=$(package_pkgbuild_var "$2" epoch "$1") || return 1
|
||||
pkgver=$(package_pkgbuild_var "$2" pkgver "$1") || return 1
|
||||
pkgrel=$(package_pkgbuild_var "$2" pkgrel "$1") || return 1
|
||||
[[ -n "$pkgver" && -n "$pkgrel" ]] || return 1
|
||||
# makepkg leaves a zero epoch out of the published version
|
||||
[[ "$epoch" != 0 ]] || epoch=""
|
||||
echo "${epoch:+$epoch:}$pkgver-$pkgrel"
|
||||
}
|
||||
|
||||
# The pkgver of a full version string, with any epoch and pkgrel removed.
|
||||
version_pkgver() {
|
||||
local version="${1#*:}"
|
||||
@@ -179,7 +273,7 @@ pkgbuild_field() {
|
||||
local package_dir="$1"
|
||||
local field="$2"
|
||||
|
||||
(cd "$package_dir" && env -u OMARCHY_SRC bash -c "source PKGBUILD 2>/dev/null; echo \"\${$field:-}\"")
|
||||
package_pkgbuild_var "$package_dir" "$field"
|
||||
}
|
||||
|
||||
# 2 -> 3, and 1.1 -> 1.2. Anything else is a pkgrel this command has no business
|
||||
@@ -252,7 +346,17 @@ record_triggers() {
|
||||
local package_dir="$1"
|
||||
local current="$2"
|
||||
|
||||
write_metadata "$package_dir" '.rebuilt_against = $current' --argjson current "$current"
|
||||
# A flat record is the legacy x86_64 shape. Preserve records for
|
||||
# architectures outside this run, then replace the ones just rebuilt.
|
||||
write_metadata "$package_dir" '
|
||||
(.rebuilt_against // {}) as $old |
|
||||
(if ($old | length) == 0 then {}
|
||||
elif ($old | to_entries | all(.value | type == "string"))
|
||||
then {x86_64: $old}
|
||||
else $old
|
||||
end) as $by_arch |
|
||||
.rebuilt_against = ($by_arch * $current)
|
||||
' --argjson current "$current"
|
||||
}
|
||||
|
||||
# Metadata that does not parse would otherwise drop its package out of the run
|
||||
@@ -300,21 +404,61 @@ sync_package() {
|
||||
|
||||
print_info "Checking $package against ${triggers[*]}..."
|
||||
|
||||
local current="{}" trigger version
|
||||
for trigger in "${triggers[@]}"; do
|
||||
version=$(repo_version "$trigger")
|
||||
if [[ -z "$version" ]]; then
|
||||
print_error " $trigger is in no official repository; leaving $package alone"
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
if ! current=$(jq -c --arg name "$trigger" --arg version "$version" '.[$name] = $version' <<<"$current"); then
|
||||
print_error " Could not record $trigger $version for $package"
|
||||
local current="{}" arch arch_current trigger version carried considered=0
|
||||
for arch in $(ci_arches); do
|
||||
package_supports_arch "$package_dir" "$arch" || continue
|
||||
considered=$((considered + 1))
|
||||
arch_current="{}"
|
||||
for trigger in "${triggers[@]}"; do
|
||||
if carried=$(carried_trigger_dir "$arch" "$trigger"); then
|
||||
if ! version=$(carried_version "$arch" "$carried"); then
|
||||
print_error " Could not read the $arch version of $trigger from its recipe; leaving $package alone"
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
load_published_versions
|
||||
if [[ -z "${EDGE_READ[$arch]:-}" ]]; then
|
||||
print_error " Could not read the published edge/$arch database for $trigger; leaving $package alone"
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
# Until edge publishes the recipe's version the builder still links
|
||||
# against the previous one, and recording the new version now would
|
||||
# certify a build that never saw it.
|
||||
if [[ "${EDGE_VERSION[$arch/$trigger]:-}" != "$version" ]]; then
|
||||
print_warning " $trigger $version is not published on edge/$arch yet; leaving $package alone until it is"
|
||||
((++SKIPPED))
|
||||
return 0
|
||||
fi
|
||||
elif ! version=$(repo_version "$arch" "$trigger"); then
|
||||
print_error " Could not read $arch repository versions; leaving $package alone"
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
if [[ -z "$version" ]]; then
|
||||
print_error " $trigger is in no $arch repository; leaving $package alone"
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
if ! arch_current=$(jq -c --arg name "$trigger" --arg version "$version" '.[$name] = $version' <<<"$arch_current"); then
|
||||
print_error " Could not record $arch/$trigger $version for $package"
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
if ! current=$(jq -c --arg arch "$arch" --argjson versions "$arch_current" '.[$arch] = $versions' <<<"$current"); then
|
||||
print_error " Could not record $arch trigger versions for $package"
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
if ((considered == 0)); then
|
||||
print_info " Skipping: not built for any published architecture"
|
||||
((++SKIPPED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
local recorded
|
||||
if ! recorded=$(package_metadata_value "$package_dir" '.rebuilt_against' ""); then
|
||||
print_error " Could not read .omarchy/package.json for $package"
|
||||
@@ -323,13 +467,20 @@ sync_package() {
|
||||
fi
|
||||
[[ -n "$recorded" && "$recorded" != "null" ]] || recorded="{}"
|
||||
|
||||
# Before architecture-specific records existed, rebuilt_against described
|
||||
# x86_64. Read it that way without forcing a metadata-only migration.
|
||||
if jq -e 'to_entries | all(.value | type == "string")' >/dev/null <<<"$recorded"; then
|
||||
recorded=$(jq -c '{x86_64: .}' <<<"$recorded")
|
||||
fi
|
||||
|
||||
# Walk the declared triggers rather than the record, so a name the record does
|
||||
# not carry reads as changed instead of going unexamined forever.
|
||||
local moved
|
||||
if ! moved=$(jq -r --argjson recorded "$recorded" '
|
||||
to_entries
|
||||
| map(select($recorded[.key] != .value)
|
||||
| "\(.key) \($recorded[.key] // "unrecorded") -> \(.value)")
|
||||
[to_entries[] as $arch
|
||||
| $arch.value | to_entries[] as $trigger
|
||||
| select($recorded[$arch.key][$trigger.key] != $trigger.value)
|
||||
| "\($arch.key)/\($trigger.key) \($recorded[$arch.key][$trigger.key] // "unrecorded") -> \($trigger.value)"]
|
||||
| join(", ")
|
||||
' <<<"$current"); then
|
||||
print_error " Could not compare recorded trigger versions for $package"
|
||||
@@ -338,7 +489,7 @@ sync_package() {
|
||||
fi
|
||||
|
||||
if [[ -z "$moved" ]]; then
|
||||
print_info " Already rebuilt against $(jq -r 'to_entries | map("\(.key) \(.value)") | join(", ")' <<<"$current")"
|
||||
print_info " Already rebuilt against every published architecture"
|
||||
((++SKIPPED))
|
||||
return 0
|
||||
fi
|
||||
@@ -384,6 +535,7 @@ sync_package() {
|
||||
# checkout that has fallen behind the repository can otherwise be bumped to
|
||||
# something pacman orders below what it would replace.
|
||||
local floor
|
||||
load_published_versions
|
||||
floor=$(published_version "$package")
|
||||
if [[ -n "$floor" && "$(version_pkgver "$floor")" == "$pkgver" ]]; then
|
||||
if [[ "$(vercmp "$new_version" "$floor")" -le 0 ]]; then
|
||||
@@ -466,11 +618,11 @@ selftest_root() {
|
||||
}
|
||||
|
||||
selftest_package() {
|
||||
local root="$1" name="$2" pkgrel="$3" metadata="$4" pkgver="${5:-1.0}"
|
||||
local root="$1" name="$2" pkgrel="$3" metadata="$4" pkgver="${5:-1.0}" arches="${6:-x86_64}"
|
||||
local dir="$root/pkgbuilds/$name"
|
||||
|
||||
mkdir -p "$dir/.omarchy"
|
||||
printf 'pkgname=%s\npkgver=%s\npkgrel=%s\narch=(x86_64)\n' "$name" "$pkgver" "$pkgrel" > "$dir/PKGBUILD"
|
||||
printf 'pkgname=%s\npkgver=%s\npkgrel=%s\narch=(%s)\n' "$name" "$pkgver" "$pkgrel" "$arches" > "$dir/PKGBUILD"
|
||||
printf '%s\n' "$metadata" > "$dir/.omarchy/package.json"
|
||||
}
|
||||
|
||||
@@ -489,44 +641,97 @@ STUB
|
||||
chmod +x "$root/stub/pacman"
|
||||
}
|
||||
|
||||
# Serves a repository database assembled by hand from name=version pairs. With
|
||||
# none given the stub fails, which is how the unreachable-repository path is
|
||||
# A repository database assembled by hand from name=version pairs.
|
||||
selftest_db() {
|
||||
local out="$1"
|
||||
shift
|
||||
local staging="$out.d" entry name version
|
||||
|
||||
rm -rf "$staging"
|
||||
mkdir -p "$staging"
|
||||
for entry in "$@"; do
|
||||
name="${entry%=*}"
|
||||
version="${entry#*=}"
|
||||
mkdir -p "$staging/$name-$version"
|
||||
printf '%%FILENAME%%\n%s-%s-x86_64.pkg.tar.zst\n\n%%NAME%%\n%s\n\n%%BASE%%\n%s\n\n%%VERSION%%\n%s\n' \
|
||||
"$name" "$version" "$name" "$name" "$version" > "$staging/$name-$version/desc"
|
||||
done
|
||||
tar --zstd -cf "$out" -C "$staging" .
|
||||
}
|
||||
|
||||
# Serves one published database for every channel and architecture. With no
|
||||
# pairs given the stub fails, which is how the unreachable-repository path is
|
||||
# exercised.
|
||||
selftest_published() {
|
||||
local root="$1"
|
||||
shift
|
||||
local staging="$root/stub/db"
|
||||
local entry name version
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
rm -rf "$staging"
|
||||
mkdir -p "$staging"
|
||||
for entry in "$@"; do
|
||||
name="${entry%=*}"
|
||||
version="${entry#*=}"
|
||||
mkdir -p "$staging/$name-$version"
|
||||
printf '%%FILENAME%%\n%s-%s-x86_64.pkg.tar.zst\n\n%%NAME%%\n%s\n\n%%BASE%%\n%s\n\n%%VERSION%%\n%s\n' \
|
||||
"$name" "$version" "$name" "$name" "$version" > "$staging/$name-$version/desc"
|
||||
done
|
||||
tar --zstd -cf "$root/stub/omarchy.db.tar.zst" -C "$staging" .
|
||||
selftest_db "$root/stub/omarchy.db.tar.zst" "$@"
|
||||
fi
|
||||
|
||||
cat > "$root/stub/curl" <<'STUB'
|
||||
#!/bin/bash
|
||||
out=""
|
||||
url=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-o) out="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
*) url="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
db="$(dirname "$0")/omarchy.db.tar.zst"
|
||||
case "$url" in
|
||||
*/omarchy.db.tar.zst)
|
||||
channel="${url%/omarchy.db.tar.zst}"
|
||||
arch="${channel##*/}"
|
||||
channel="${channel%/*}"
|
||||
db="$(dirname "$0")/omarchy-${channel##*/}-$arch.db.tar.zst"
|
||||
[[ -f "$db" ]] || db="$(dirname "$0")/omarchy.db.tar.zst"
|
||||
;;
|
||||
*/aarch64/*)
|
||||
repo="${url%/*}"
|
||||
db="$(dirname "$0")/alarm-${repo##*/}.db"
|
||||
;;
|
||||
*) db="" ;;
|
||||
esac
|
||||
[[ -f "$db" && -n "$out" ]] || exit 22
|
||||
cp "$db" "$out"
|
||||
STUB
|
||||
chmod +x "$root/stub/curl"
|
||||
}
|
||||
|
||||
# Serves one channel and architecture its own published database.
|
||||
selftest_channel() { # selftest_channel <root> <channel> <arch> [name=version...]
|
||||
local root="$1" channel="$2" arch="$3"
|
||||
shift 3
|
||||
selftest_db "$root/stub/omarchy-$channel-$arch.db.tar.zst" "$@"
|
||||
}
|
||||
|
||||
selftest_alarm() {
|
||||
local root="$1"
|
||||
shift
|
||||
local repo staging="$root/stub/alarm-db" entry name version
|
||||
|
||||
for repo in core extra; do
|
||||
rm -rf "$staging"
|
||||
mkdir -p "$staging"
|
||||
if [[ "$repo" == "core" ]]; then
|
||||
mkdir -p "$staging/unrelated-1-1"
|
||||
printf '%%FILENAME%%\nunrelated-1-1-aarch64.pkg.tar.zst\n\n%%NAME%%\nunrelated\n\n%%BASE%%\nunrelated\n\n%%VERSION%%\n1-1\n' \
|
||||
> "$staging/unrelated-1-1/desc"
|
||||
else
|
||||
for entry in "$@"; do
|
||||
name="${entry%=*}"
|
||||
version="${entry#*=}"
|
||||
mkdir -p "$staging/$name-$version"
|
||||
printf '%%FILENAME%%\n%s-%s-aarch64.pkg.tar.zst\n\n%%NAME%%\n%s\n\n%%BASE%%\n%s\n\n%%VERSION%%\n%s\n' \
|
||||
"$name" "$version" "$name" "$name" "$version" > "$staging/$name-$version/desc"
|
||||
done
|
||||
fi
|
||||
tar -czf "$root/stub/alarm-$repo.db" -C "$staging" .
|
||||
done
|
||||
}
|
||||
|
||||
cmd_self_test() {
|
||||
local failures=0
|
||||
local root
|
||||
@@ -541,11 +746,15 @@ cmd_self_test() {
|
||||
fi
|
||||
}
|
||||
|
||||
# SELFTEST_ARCHES stands in for CI_ARCHES; "default" leaves it unset, as the
|
||||
# scheduled workflow does.
|
||||
run_case() {
|
||||
local root="$1"
|
||||
shift
|
||||
local status=0
|
||||
PATH="$root/stub:$PATH" "$root/bin/sync-rebuilds" "$@" > "$root/output" 2>&1 || status=$?
|
||||
local status=0 arches=(CI_ARCHES="${SELFTEST_ARCHES:-x86_64}")
|
||||
[[ "${SELFTEST_ARCHES:-}" != default ]] || arches=(-u CI_ARCHES)
|
||||
env "${arches[@]}" PATH="$root/stub:$PATH" \
|
||||
"$root/bin/sync-rebuilds" "$@" > "$root/output" 2>&1 || status=$?
|
||||
echo "$status"
|
||||
}
|
||||
|
||||
@@ -563,7 +772,7 @@ cmd_self_test() {
|
||||
check "run succeeds" 0 "$(run_case "$root")"
|
||||
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-partial")"
|
||||
check "unrecorded trigger now recorded" "2-2" \
|
||||
"$(jq -r '.rebuilt_against["dep-b"]' "$root/pkgbuilds/t-partial/.omarchy/package.json")"
|
||||
"$(jq -r '.rebuilt_against.x86_64["dep-b"]' "$root/pkgbuilds/t-partial/.omarchy/package.json")"
|
||||
|
||||
echo "Opting a package in buys a rebuild rather than a bare record:"
|
||||
root=$(selftest_root fresh)
|
||||
@@ -573,7 +782,7 @@ cmd_self_test() {
|
||||
check "run succeeds" 0 "$(run_case "$root")"
|
||||
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-fresh")"
|
||||
check "trigger recorded" "1-1" \
|
||||
"$(jq -r '.rebuilt_against["dep-a"]' "$root/pkgbuilds/t-fresh/.omarchy/package.json")"
|
||||
"$(jq -r '.rebuilt_against.x86_64["dep-a"]' "$root/pkgbuilds/t-fresh/.omarchy/package.json")"
|
||||
|
||||
echo "An unchanged package is left alone:"
|
||||
root=$(selftest_root current)
|
||||
@@ -612,6 +821,138 @@ cmd_self_test() {
|
||||
check "suffix recorded for the next AUR sync" 1 \
|
||||
"$(jq -r '.pkgrel.suffix' "$root/pkgbuilds/t-aur/.omarchy/package.json")"
|
||||
|
||||
echo "A dependency is tracked independently for both published architectures:"
|
||||
root=$(selftest_root multiarch)
|
||||
selftest_package "$root" t-multi 1 '{"source":"local","rebuild_on":["dep-a"],"rebuilt_against":{"dep-a":"1-1"}}' 1.0 'x86_64 aarch64'
|
||||
selftest_pacman "$root" dep-a=1-1
|
||||
selftest_published "$root"
|
||||
selftest_alarm "$root" dep-a=2-1
|
||||
SELFTEST_ARCHES="x86_64 aarch64"
|
||||
check "run succeeds" 0 "$(run_case "$root")"
|
||||
unset SELFTEST_ARCHES
|
||||
check "pkgrel bumped once" 2 "$(pkgrel_of "$root/pkgbuilds/t-multi")"
|
||||
check "x86_64 trigger recorded" "1-1" \
|
||||
"$(jq -r '.rebuilt_against.x86_64["dep-a"]' "$root/pkgbuilds/t-multi/.omarchy/package.json")"
|
||||
check "aarch64 trigger recorded" "2-1" \
|
||||
"$(jq -r '.rebuilt_against.aarch64["dep-a"]' "$root/pkgbuilds/t-multi/.omarchy/package.json")"
|
||||
|
||||
echo "An ARM-only run records only the ARM dependency state:"
|
||||
root=$(selftest_root arm-only)
|
||||
selftest_package "$root" t-arm 1 '{"source":"local","rebuild_on":["dep-a"]}' 1.0 'x86_64 aarch64'
|
||||
selftest_pacman "$root"
|
||||
selftest_published "$root"
|
||||
selftest_alarm "$root" dep-a=2-1
|
||||
SELFTEST_ARCHES=aarch64
|
||||
check "run succeeds" 0 "$(run_case "$root")"
|
||||
unset SELFTEST_ARCHES
|
||||
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-arm")"
|
||||
check "ARM trigger recorded" "2-1" \
|
||||
"$(jq -r '.rebuilt_against.aarch64["dep-a"]' "$root/pkgbuilds/t-arm/.omarchy/package.json")"
|
||||
check "x86_64 was not consulted" "false" \
|
||||
"$(jq -r '.rebuilt_against | has("x86_64")' "$root/pkgbuilds/t-arm/.omarchy/package.json")"
|
||||
|
||||
echo "An x86-only package ignores ARM during a dual-architecture run:"
|
||||
root=$(selftest_root x86-package)
|
||||
selftest_package "$root" t-x86 1 '{"source":"local","rebuild_on":["dep-a"],"rebuilt_against":{"dep-a":"1-1"}}'
|
||||
selftest_pacman "$root" dep-a=1-1
|
||||
selftest_published "$root"
|
||||
selftest_alarm "$root" dep-a=2-1
|
||||
SELFTEST_ARCHES="x86_64 aarch64"
|
||||
check "run succeeds" 0 "$(run_case "$root")"
|
||||
unset SELFTEST_ARCHES
|
||||
check "pkgrel untouched" 1 "$(pkgrel_of "$root/pkgbuilds/t-x86")"
|
||||
|
||||
echo "An ARM-only package is checked when no architecture list is given:"
|
||||
root=$(selftest_root arm-default)
|
||||
selftest_package "$root" t-arm-default 1 '{"source":"local","rebuild_on":["dep-a"],"rebuilt_against":{"aarch64":{"dep-a":"1-1"}}}' 1.0 aarch64
|
||||
selftest_pacman "$root"
|
||||
selftest_published "$root"
|
||||
selftest_alarm "$root" dep-a=2-1
|
||||
SELFTEST_ARCHES=default
|
||||
check "run succeeds" 0 "$(run_case "$root")"
|
||||
unset SELFTEST_ARCHES
|
||||
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-arm-default")"
|
||||
check "ARM trigger recorded" "2-1" \
|
||||
"$(jq -r '.rebuilt_against.aarch64["dep-a"]' "$root/pkgbuilds/t-arm-default/.omarchy/package.json")"
|
||||
|
||||
echo "A dependency carried here for ARM is read from its recipe once edge publishes it:"
|
||||
root=$(selftest_root carried)
|
||||
selftest_package "$root" t-carried 1.1 '{"source":"local","channels":["edge"]}' 0.15.1 aarch64
|
||||
selftest_package "$root" t-linked 3 '{"source":"local","rebuild_on":["t-carried"],"rebuilt_against":{"x86_64":{"t-carried":"0.15.1-1"},"aarch64":{"t-carried":"0.15.0-2"}}}' 1.0 'x86_64 aarch64'
|
||||
selftest_pacman "$root" t-carried=0.15.1-1
|
||||
selftest_published "$root"
|
||||
selftest_alarm "$root" t-carried=0.15.1-1
|
||||
selftest_channel "$root" edge aarch64 t-carried=0.15.1-1.1
|
||||
SELFTEST_ARCHES=default
|
||||
check "run succeeds" 0 "$(run_case "$root")"
|
||||
check "pkgrel bumped" 4 "$(pkgrel_of "$root/pkgbuilds/t-linked")"
|
||||
check "the carried version is recorded, not Arch Linux ARM's" "0.15.1-1.1" \
|
||||
"$(jq -r '.rebuilt_against.aarch64["t-carried"]' "$root/pkgbuilds/t-linked/.omarchy/package.json")"
|
||||
check "x86_64, where nothing is carried, still reads the distribution" "0.15.1-1" \
|
||||
"$(jq -r '.rebuilt_against.x86_64["t-carried"]' "$root/pkgbuilds/t-linked/.omarchy/package.json")"
|
||||
check "a second run succeeds" 0 "$(run_case "$root")"
|
||||
unset SELFTEST_ARCHES
|
||||
check "a second run leaves it alone" 4 "$(pkgrel_of "$root/pkgbuilds/t-linked")"
|
||||
|
||||
echo "A carried dependency that edge does not publish yet leaves its dependents alone:"
|
||||
root=$(selftest_root carried-in-flight)
|
||||
selftest_package "$root" t-carried 1.2 '{"source":"local","channels":["edge"]}' 0.15.1 aarch64
|
||||
selftest_package "$root" t-linked 4 '{"source":"local","rebuild_on":["t-carried"],"rebuilt_against":{"aarch64":{"t-carried":"0.15.1-1.1"}}}' 1.0 aarch64
|
||||
selftest_pacman "$root"
|
||||
selftest_published "$root"
|
||||
selftest_alarm "$root" t-carried=0.15.1-1
|
||||
selftest_channel "$root" edge aarch64 t-carried=0.15.1-1.1
|
||||
SELFTEST_ARCHES=aarch64
|
||||
check "run succeeds" 0 "$(run_case "$root")"
|
||||
unset SELFTEST_ARCHES
|
||||
check "pkgrel untouched" 4 "$(pkgrel_of "$root/pkgbuilds/t-linked")"
|
||||
check "record untouched" "0.15.1-1.1" \
|
||||
"$(jq -r '.rebuilt_against.aarch64["t-carried"]' "$root/pkgbuilds/t-linked/.omarchy/package.json")"
|
||||
|
||||
echo "A carried dependency whose edge database cannot be read fails the run:"
|
||||
root=$(selftest_root carried-unreadable)
|
||||
selftest_package "$root" t-carried 1.1 '{"source":"local","channels":["edge"]}' 0.15.1 aarch64
|
||||
selftest_package "$root" t-linked 3 '{"source":"local","rebuild_on":["t-carried"],"rebuilt_against":{"aarch64":{"t-carried":"0.15.0-2"}}}' 1.0 aarch64
|
||||
selftest_pacman "$root"
|
||||
selftest_published "$root"
|
||||
selftest_alarm "$root" t-carried=0.15.1-1
|
||||
SELFTEST_ARCHES=aarch64
|
||||
check "run fails" 1 "$(run_case "$root")"
|
||||
check "pkgrel untouched" 3 "$(pkgrel_of "$root/pkgbuilds/t-linked")"
|
||||
printf 'not a database\n' > "$root/stub/omarchy-edge-aarch64.db.tar.zst"
|
||||
check "a corrupt download fails the run too" 1 "$(run_case "$root")"
|
||||
unset SELFTEST_ARCHES
|
||||
check "pkgrel still untouched" 3 "$(pkgrel_of "$root/pkgbuilds/t-linked")"
|
||||
|
||||
echo "A PKGBUILD that branches on CARCH at file scope still reads its version:"
|
||||
root=$(selftest_root carch-branch)
|
||||
mkdir -p "$root/pkgbuilds/t-carch/.omarchy"
|
||||
cat > "$root/pkgbuilds/t-carch/PKGBUILD" <<'PKG'
|
||||
pkgname=t-carch
|
||||
case "$CARCH" in
|
||||
x86_64) _suffix=x64 ;;
|
||||
aarch64) _suffix=arm64 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
pkgver=1.0
|
||||
pkgrel=3
|
||||
arch=(x86_64 aarch64)
|
||||
PKG
|
||||
echo '{"source":"local"}' > "$root/pkgbuilds/t-carch/.omarchy/package.json"
|
||||
check "pkgver read for x86_64" "1.0" "$(package_pkgbuild_var "$root/pkgbuilds/t-carch" pkgver x86_64)"
|
||||
check "pkgrel read for x86_64" "3" "$(package_pkgbuild_var "$root/pkgbuilds/t-carch" pkgrel x86_64)"
|
||||
check "arch-specific value follows CARCH" "arm64" "$(package_pkgbuild_var "$root/pkgbuilds/t-carch" _suffix aarch64)"
|
||||
check "unsupported arch reports failure" 1 "$(package_pkgbuild_var "$root/pkgbuilds/t-carch" pkgver armv7h >/dev/null; echo $?)"
|
||||
|
||||
echo "A carried recipe's version is spelled the way makepkg publishes it:"
|
||||
root=$(selftest_root carried-epoch)
|
||||
selftest_package "$root" t-epoch 1.1 '{"source":"local"}' 0.15.1 aarch64
|
||||
check "no epoch" "0.15.1-1.1" "$(carried_version aarch64 "$root/pkgbuilds/t-epoch")"
|
||||
echo 'epoch=0' >> "$root/pkgbuilds/t-epoch/PKGBUILD"
|
||||
check "a zero epoch is left out" "0.15.1-1.1" "$(carried_version aarch64 "$root/pkgbuilds/t-epoch")"
|
||||
echo 'epoch=2' >> "$root/pkgbuilds/t-epoch/PKGBUILD"
|
||||
check "a real epoch is kept" "2:0.15.1-1.1" "$(carried_version aarch64 "$root/pkgbuilds/t-epoch")"
|
||||
|
||||
echo ""
|
||||
if [[ "$failures" -eq 0 ]]; then
|
||||
print_success "Self-test passed"
|
||||
@@ -626,9 +967,9 @@ if [[ "$SELF_TEST" == true ]]; then
|
||||
exit $?
|
||||
fi
|
||||
|
||||
for tool in pacman vercmp jq curl; do
|
||||
for tool in pacman vercmp jq curl tar; do
|
||||
if ! command -v "$tool" >/dev/null 2>&1; then
|
||||
print_error "$tool not found: reading trigger versions and ordering pkgrels both need pacman"
|
||||
print_error "$tool not found: rebuild trigger sync cannot run"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
+486
-23
@@ -9,8 +9,10 @@ source "$BUILD_ROOT/helpers/upstream-github.sh"
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
export UPSTREAM_CACHE_DIR="$TEMP_DIR/watch-cache"
|
||||
|
||||
SPECIFIC_PACKAGES=()
|
||||
LANE=all
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
@@ -18,11 +20,11 @@ Usage: $0 [PACKAGE...]
|
||||
|
||||
Update packages that track an upstream vendor release feed instead of the AUR.
|
||||
|
||||
A package whose upstream ships tagged GitHub releases with a checksum manifest
|
||||
opts in declaratively, via "upstream" in .omarchy/package.json (see
|
||||
helpers/upstream-github.sh for the schema); no code needed. Anything with a
|
||||
bespoke feed provides pkgbuilds/<package>/.omarchy/upstream.sh instead, a hook
|
||||
that reports the newest upstream release as JSON on stdout:
|
||||
Packages opt in declaratively through "upstream" in .omarchy/package.json.
|
||||
Providers cover GitHub Releases with checksum manifests or API asset digests,
|
||||
semver-shaped git tags, npm dist-tags, and plain Debian Packages indexes. See
|
||||
README.md for the schemas. Anything outside those conventions may provide
|
||||
pkgbuilds/<package>/.omarchy/upstream.sh, a hook that reports JSON on stdout:
|
||||
|
||||
{
|
||||
"pkgver": "1.2.3",
|
||||
@@ -48,6 +50,14 @@ the bypass, so the resulting change still goes through a reviewed PR.
|
||||
Arguments:
|
||||
PACKAGE One or more package names to update (optional)
|
||||
|
||||
Options:
|
||||
--lane <reviewed|auto-merge|all>
|
||||
Which delivery lane to sync (default: all). Packages marked
|
||||
"auto_merge": true ride the unattended lane (the branch tracker
|
||||
opens and auto-merges their PR); everything else is reviewed.
|
||||
The scheduled workflows each pass their own lane so a moving
|
||||
branch tip never waits on a vendor release, or the reverse.
|
||||
|
||||
Commands:
|
||||
self-test Run the offline fixture tests for release selection, the
|
||||
quarantine backstop, and metadata parsing
|
||||
@@ -64,6 +74,14 @@ while [[ $# -gt 0 ]]; do
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--lane)
|
||||
LANE="${2:-}"
|
||||
case "$LANE" in
|
||||
reviewed|auto-merge|all) ;;
|
||||
*) print_error "Invalid --lane '$LANE' (expected reviewed, auto-merge, or all)"; exit 1 ;;
|
||||
esac
|
||||
shift 2
|
||||
;;
|
||||
--*)
|
||||
print_error "Unknown option: $1"
|
||||
exit 1
|
||||
@@ -140,17 +158,22 @@ set_pkgbuild_array() {
|
||||
|
||||
local rewritten="$TEMP_DIR/pkgbuild-rewritten"
|
||||
if ! awk -v prefix="${name}=(" -v block="$block" '
|
||||
# The code on a line, minus any comment. Checksum arrays hold quoted hex
|
||||
# (or SKIP), so a "#" can only ever start a comment here.
|
||||
function code(s) { sub(/#.*/, "", s); return s }
|
||||
!replaced && index($0, prefix) == 1 {
|
||||
while ((getline line < block) > 0) print line
|
||||
close(block)
|
||||
replaced = 1
|
||||
# A ")" anywhere past the opening closes the array; testing for one at end
|
||||
# of line instead would treat a trailing comment as a continuation and eat
|
||||
# every line up to the next ")".
|
||||
if (index(substr($0, length(prefix) + 1), ")") == 0) skipping = 1
|
||||
# every line up to the next ")". A ")" inside a comment -- "# sidecar
|
||||
# (new in v0.7.5)" -- closes nothing, and ending the skip there would
|
||||
# leave the rest of the old array behind a stray ")".
|
||||
if (index(code(substr($0, length(prefix) + 1)), ")") == 0) skipping = 1
|
||||
next
|
||||
}
|
||||
skipping { if ($0 ~ /\)/) skipping = 0; next }
|
||||
skipping { if (code($0) ~ /\)/) skipping = 0; next }
|
||||
{ print }
|
||||
' "$pkgbuild" > "$rewritten"; then
|
||||
print_error "Failed to rewrite ${name} in $pkgbuild"
|
||||
@@ -332,27 +355,33 @@ sync_package() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
local github_repo has_upstream=false
|
||||
github_repo=$(package_upstream_github_repo "$package_dir")
|
||||
if jq -e '.sync == false' "$package_dir/.omarchy/package.json" >/dev/null 2>&1; then
|
||||
print_info "Skipping $package: upstream updates held by sync=false"
|
||||
((++SKIPPED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
local provider has_upstream=false
|
||||
provider=$(package_upstream_provider "$package_dir")
|
||||
if package_has_upstream_provider "$package_dir"; then
|
||||
has_upstream=true
|
||||
fi
|
||||
|
||||
# A present-but-unusable declaration fails loudly; treating it like "no
|
||||
# upstream source" would silently drop the package from scheduled runs.
|
||||
if [[ "$has_upstream" == true && -z "$github_repo" ]]; then
|
||||
print_error "Package $package has an unusable upstream declaration (needs a github owner/repo)"
|
||||
if [[ "$has_upstream" == true && -z "$provider" ]]; then
|
||||
print_error "Package $package has an unusable or ambiguous upstream declaration"
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "$github_repo" && -f "$hook" ]]; then
|
||||
print_error "Package $package declares both upstream.github and an upstream.sh hook; keep exactly one"
|
||||
if [[ -n "$provider" && -f "$hook" ]]; then
|
||||
print_error "Package $package declares both an upstream provider and an upstream.sh hook; keep exactly one"
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "$github_repo" && ! -f "$hook" ]]; then
|
||||
if [[ -z "$provider" && ! -f "$hook" ]]; then
|
||||
if [[ "$SPECIFIC_MODE" == true ]]; then
|
||||
print_error "Package $package has neither an upstream declaration nor .omarchy/upstream.sh"
|
||||
((++FAILED))
|
||||
@@ -372,10 +401,31 @@ sync_package() {
|
||||
|
||||
print_info "Checking $package for upstream releases..."
|
||||
|
||||
local release
|
||||
if [[ -n "$github_repo" ]]; then
|
||||
if ! release=$(github_upstream_release "$package_dir" "$min_age"); then
|
||||
print_error "GitHub release provider failed for $package"
|
||||
if [[ "$provider" == watch ]]; then
|
||||
local result
|
||||
if ! result=$(python3 "$BUILD_ROOT/helpers/upstream-watch.py" sync "$package_dir" --min-age "$min_age"); then
|
||||
print_error "Upstream watch failed for $package"
|
||||
((++FAILED))
|
||||
elif [[ $(jq -r .status <<<"$result") == updated ]]; then
|
||||
print_success " $(jq -r '.before + " -> " + .after' <<<"$result")"
|
||||
((++UPDATED))
|
||||
else
|
||||
print_info " $(jq -r '.reason' <<<"$result")"
|
||||
((++SKIPPED))
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
local release release_status=0
|
||||
if [[ -n "$provider" ]]; then
|
||||
case "$provider" in
|
||||
github) release=$(github_upstream_release "$package_dir" "$min_age") || release_status=$? ;;
|
||||
git_tags) release=$(git_tags_upstream_release "$package_dir") || release_status=$? ;;
|
||||
npm) release=$(npm_upstream_release "$package_dir") || release_status=$? ;;
|
||||
debian) release=$(debian_upstream_release "$package_dir") || release_status=$? ;;
|
||||
esac
|
||||
if [[ ${release_status:-0} -ne 0 ]]; then
|
||||
print_error "$provider upstream provider failed for $package"
|
||||
((++FAILED))
|
||||
return 0
|
||||
fi
|
||||
@@ -461,8 +511,8 @@ sync_package() {
|
||||
# paths. Covers release selection (fallback past quarantined releases,
|
||||
# draft/prerelease filtering, bypass, unchanged version), failure paths
|
||||
# (unusable tags/timestamps, missing checksums), checksum template mapping
|
||||
# for both architectures, the min_release_age backstop, the duration parser,
|
||||
# and manifest validation.
|
||||
# for both architectures, release API digests, the min_release_age backstop,
|
||||
# the duration parser, and manifest validation.
|
||||
cmd_self_test() {
|
||||
local failures=0
|
||||
|
||||
@@ -539,6 +589,31 @@ EOF
|
||||
check "bypass lifts the quarantine" "2.0.0" "$(jq -r '.pkgver // "<none>"' <<<"$out")"
|
||||
check "x86_64 checksum via * binary-mode prefix" "$sum_x20" "$(jq -r '.sha256sums.x86_64[0] // "<none>"' <<<"$out")"
|
||||
|
||||
cp "$pkg/.omarchy/package.json" "$pkg/normal.json"
|
||||
jq '.upstream.latest_only = true' "$pkg/normal.json" > "$pkg/.omarchy/package.json"
|
||||
FIXTURE_RELEASES=$(jq -n --arg old "$old2d" '[
|
||||
{tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false},
|
||||
{tag_name: "v0.4.1-8", published_at: $old, draft: false, prerelease: false}
|
||||
]')
|
||||
FIXTURE_CHECKSUMS=$(printf '%s\n' \
|
||||
"$sum_x19 ./tool-v1.9.0-x64.tar.xz" \
|
||||
"$sum_a19 ./tool-v1.9.0-arm64.tar.xz")
|
||||
out=$(github_upstream_release "$pkg" 0 2>/dev/null) || out="<error>"
|
||||
check "latest_only ignores incompatible historical tags" "1.9.0" "$(jq -r '.pkgver // "<none>"' <<<"$out")"
|
||||
cp "$pkg/normal.json" "$pkg/.omarchy/package.json"
|
||||
FIXTURE_RELEASES=$(jq -n --arg young "$young" --arg old2 "$old2d" --arg old3 "$old3d" '[
|
||||
{tag_name: "v2.0.0", published_at: $young, draft: false, prerelease: false},
|
||||
{tag_name: "v1.9.9", published_at: $old2, draft: false, prerelease: true},
|
||||
{tag_name: "v1.9.8", published_at: $old2, draft: true, prerelease: false},
|
||||
{tag_name: "v1.9.0", published_at: $old2, draft: false, prerelease: false},
|
||||
{tag_name: "v1.8.0", published_at: $old3, draft: false, prerelease: false}
|
||||
]')
|
||||
FIXTURE_CHECKSUMS=$(printf '%s\n' \
|
||||
"$sum_x19 ./tool-v1.9.0-x64.tar.xz" \
|
||||
"$sum_a19 tool-v1.9.0-arm64.tar.xz" \
|
||||
"$sum_x20 *tool-v2.0.0-x64.tar.xz" \
|
||||
"$sum_a20 tool-v2.0.0-arm64.tar.xz")
|
||||
|
||||
out=$(github_upstream_release "$pkg" 8640000 2>/dev/null) || out="<error>"
|
||||
check "everything quarantined reports no update" "{}" "$(jq -c . <<<"$out")"
|
||||
|
||||
@@ -562,6 +637,190 @@ EOF
|
||||
rc=0; github_upstream_release "$pkg" 86400 >/dev/null 2>&1 || rc=$?
|
||||
check "missing aarch64 checksum fails the sync" "1" "$rc"
|
||||
|
||||
# A vendor publishing no manifest: checksums come from the digests the
|
||||
# release API reports per asset, with nothing fetched beyond the feed.
|
||||
echo "Release API digests:"
|
||||
local digpkg="$TEMP_DIR/selftest-digests"
|
||||
mkdir -p "$digpkg/.omarchy"
|
||||
printf 'pkgver=1.0.0\npkgrel=1\n' > "$digpkg/PKGBUILD"
|
||||
cat > "$digpkg/.omarchy/package.json" <<'EOF'
|
||||
{
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"github": "example/tool",
|
||||
"digests": true,
|
||||
"assets": {
|
||||
"x86_64": "tool-{pkgver}-1-x86_64.pkg.tar.zst",
|
||||
"aarch64": "tool-{pkgver}-1-aarch64.pkg.tar.zst"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" --arg a "$sum_a19" '[
|
||||
{tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [
|
||||
{name: "tool-1.9.0-1-x86_64.pkg.tar.zst", digest: ("sha256:" + $x)},
|
||||
{name: "tool-1.9.0-1-aarch64.pkg.tar.zst", digest: ("sha256:" + $a)},
|
||||
{name: "tool-1.9.0-1-x86_64.rpm", digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000"}
|
||||
]}
|
||||
]')
|
||||
FIXTURE_CHECKSUMS="manifest must not be consulted"
|
||||
out=$(github_upstream_release "$digpkg" 0 2>/dev/null) || out="<error>"
|
||||
check "x86_64 checksum via the asset digest" "$sum_x19" "$(jq -r '.sha256sums.x86_64[0] // "<none>"' <<<"$out")"
|
||||
check "aarch64 checksum via the asset digest" "$sum_a19" "$(jq -r '.sha256sums.aarch64[0] // "<none>"' <<<"$out")"
|
||||
|
||||
FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" '[
|
||||
{tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [
|
||||
{name: "tool-1.9.0-1-x86_64.pkg.tar.zst", digest: ("sha256:" + $x)},
|
||||
{name: "tool-1.9.0-1-aarch64.pkg.tar.zst"}
|
||||
]}
|
||||
]')
|
||||
rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$?
|
||||
check "asset without a digest fails the sync" "1" "$rc"
|
||||
|
||||
FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" '[
|
||||
{tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [
|
||||
{name: "tool-1.9.0-1-x86_64.pkg.tar.zst", digest: ("sha256:" + $x)},
|
||||
{name: "tool-1.9.0-2-aarch64.pkg.tar.zst", digest: ("sha256:" + $x)}
|
||||
]}
|
||||
]')
|
||||
rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$?
|
||||
check "asset re-cut under another release number fails the sync" "1" "$rc"
|
||||
|
||||
FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" --arg a "$sum_a19" '[
|
||||
{tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [
|
||||
{name: "tool-1.9.0-1-x86_64.pkg.tar.zst", digest: $x},
|
||||
{name: "tool-1.9.0-1-aarch64.pkg.tar.zst", digest: ("sha256:" + $a)}
|
||||
]}
|
||||
]')
|
||||
rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$?
|
||||
check "digest without the sha256: prefix fails the sync" "1" "$rc"
|
||||
|
||||
# The provider enforces the declaration shape itself: scheduled runs reach
|
||||
# it without validate_package_metadata.
|
||||
jq '.upstream.checksums = "SHASUMS256.txt"' "$digpkg/.omarchy/package.json" > "$digpkg/both.json"
|
||||
cp "$digpkg/.omarchy/package.json" "$digpkg/good.json"
|
||||
cp "$digpkg/both.json" "$digpkg/.omarchy/package.json"
|
||||
rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$?
|
||||
check "provider rejects checksums and digests together" "1" "$rc"
|
||||
jq '.upstream.digests = "true"' "$digpkg/good.json" > "$digpkg/.omarchy/package.json"
|
||||
rc=0; github_upstream_release "$digpkg" 0 >/dev/null 2>&1 || rc=$?
|
||||
check "provider rejects a non-boolean digests" "1" "$rc"
|
||||
cp "$digpkg/good.json" "$digpkg/.omarchy/package.json"
|
||||
|
||||
echo "Ordered GitHub assets with supplemental sources:"
|
||||
local multipkg="$TEMP_DIR/selftest-multi-assets" multi_sum support_sum multi_vst
|
||||
mkdir -p "$multipkg/.omarchy"
|
||||
printf 'pkgver=1.0.0\npkgrel=1\n' > "$multipkg/PKGBUILD"
|
||||
cat > "$multipkg/.omarchy/package.json" <<'EOF'
|
||||
{
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"github": "example/tool",
|
||||
"digests": true,
|
||||
"assets": {
|
||||
"x86_64": ["tool-{pkgver}-x86_64", "tool-{pkgver}-x86_64.asc"],
|
||||
"aarch64": ["tool-{pkgver}-aarch64", "tool-{pkgver}-aarch64.asc"]
|
||||
},
|
||||
"sources": {
|
||||
"any": ["https://example.test/tool/{tag}/support.txt"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
local sum_x19_sig sum_a19_sig
|
||||
sum_x19_sig=$(printf '1%.0s' {1..64})
|
||||
sum_a19_sig=$(printf '2%.0s' {1..64})
|
||||
FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" --arg xs "$sum_x19_sig" \
|
||||
--arg a "$sum_a19" --arg as "$sum_a19_sig" '[
|
||||
{tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [
|
||||
{name: "tool-1.9.0-x86_64", digest: ("sha256:" + $x)},
|
||||
{name: "tool-1.9.0-x86_64.asc", digest: ("sha256:" + $xs)},
|
||||
{name: "tool-1.9.0-aarch64", digest: ("sha256:" + $a)},
|
||||
{name: "tool-1.9.0-aarch64.asc", digest: ("sha256:" + $as)}
|
||||
]}
|
||||
]')
|
||||
upstream_fetch_source() { printf 'remote fixture for %s\n' "$1" > "$2"; }
|
||||
multi_sum=$(github_upstream_release "$multipkg" 0 2>/dev/null) || multi_sum="<error>"
|
||||
support_sum=$(printf 'remote fixture for %s\n' 'https://example.test/tool/v1.9.0/support.txt' | sha256sum | cut -d' ' -f1)
|
||||
check "ordered GitHub assets produce an ordered checksum array" "$sum_x19 $sum_x19_sig" \
|
||||
"$(jq -r '.sha256sums.x86_64 | join(" ")' <<<"$multi_sum")"
|
||||
check "GitHub supplemental source is downloaded and hashed" "$support_sum" \
|
||||
"$(jq -r '.sha256sums.any[0]' <<<"$multi_sum")"
|
||||
multi_vst=0; validate_package_metadata "$multipkg" >/dev/null || multi_vst=$?
|
||||
check "GitHub asset lists and disjoint sources validate" "0" "$multi_vst"
|
||||
jq '.upstream.sources.x86_64 = ["https://example.test/duplicate"]' \
|
||||
"$multipkg/.omarchy/package.json" > "$multipkg/overlap.json"
|
||||
cp "$multipkg/.omarchy/package.json" "$multipkg/good.json"
|
||||
cp "$multipkg/overlap.json" "$multipkg/.omarchy/package.json"
|
||||
multi_vst=0; validate_package_metadata "$multipkg" >/dev/null || multi_vst=$?
|
||||
check "GitHub assets and sources cannot target the same checksum array" "1" "$multi_vst"
|
||||
cp "$multipkg/good.json" "$multipkg/.omarchy/package.json"
|
||||
|
||||
echo "Debian Packages provider:"
|
||||
local debpkg="$TEMP_DIR/selftest-debian" deb_sum deb_x_sum deb_a_sum deb_vst
|
||||
mkdir -p "$debpkg/.omarchy"
|
||||
printf 'pkgver=1.0.0\npkgrel=1\nsha256sums_x86_64=("old")\nsha256sums_aarch64=("old")\n' > "$debpkg/PKGBUILD"
|
||||
cat > "$debpkg/.omarchy/package.json" <<'EOF'
|
||||
{
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"debian": "https://packages.example.test/dists/stable/main/binary-amd64/Packages",
|
||||
"package": "example-app",
|
||||
"sources": {
|
||||
"x86_64": ["https://downloads.example.test/app-{pkgver}-x64.tar.gz"],
|
||||
"aarch64": ["https://downloads.example.test/app-{pkgver}-arm64.tar.gz"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
debian_fetch_packages() {
|
||||
cat <<'EOF'
|
||||
Package: unrelated
|
||||
Version: 99.0.0
|
||||
|
||||
Package: example-app
|
||||
Version: 1.9.0
|
||||
|
||||
Package: example-app
|
||||
Version: 1.10.0
|
||||
EOF
|
||||
}
|
||||
deb_sum=$(debian_upstream_release "$debpkg")
|
||||
deb_x_sum=$(printf 'remote fixture for %s\n' 'https://downloads.example.test/app-1.10.0-x64.tar.gz' | sha256sum | cut -d' ' -f1)
|
||||
deb_a_sum=$(printf 'remote fixture for %s\n' 'https://downloads.example.test/app-1.10.0-arm64.tar.gz' | sha256sum | cut -d' ' -f1)
|
||||
check "Debian provider selects the newest exact package stanza" "1.10.0" "$(jq -r '.pkgver' <<<"$deb_sum")"
|
||||
check "Debian x86_64 source is hashed" "$deb_x_sum" "$(jq -r '.sha256sums.x86_64[0]' <<<"$deb_sum")"
|
||||
check "Debian aarch64 source is hashed" "$deb_a_sum" "$(jq -r '.sha256sums.aarch64[0]' <<<"$deb_sum")"
|
||||
deb_vst=0; validate_package_metadata "$debpkg" >/dev/null || deb_vst=$?
|
||||
check "Debian declaration validates" "0" "$deb_vst"
|
||||
printf 'pkgver=1.10.0\npkgrel=1\nsha256sums_x86_64=("old")\nsha256sums_aarch64=("old")\n' > "$debpkg/PKGBUILD"
|
||||
check "checked-in Debian version avoids source downloads" "{}" "$(debian_upstream_release "$debpkg" | jq -c .)"
|
||||
|
||||
echo "End-to-end Debian sync with the checked-in 1password recipe:"
|
||||
local one_root="$TEMP_DIR/e2e-1password" one_dir one_version=8.12.35
|
||||
mkdir -p "$one_root"
|
||||
cp -a "$BUILD_ROOT/pkgbuilds/1password" "$one_root/1password"
|
||||
one_dir="$one_root/1password"
|
||||
# Keep the real recipe shape, but make the fixture's starting version stable.
|
||||
# Otherwise a routine package update can outrun the mocked release below.
|
||||
sed -i -e 's/^pkgver=.*/pkgver=8.12.34/' -e 's/^pkgrel=.*/pkgrel=2/' "$one_dir/PKGBUILD"
|
||||
debian_fetch_packages() {
|
||||
printf 'Package: 1password\nVersion: %s\n' "$one_version"
|
||||
}
|
||||
upstream_fetch_source() { printf 'remote fixture for %s\n' "$1" > "$2"; }
|
||||
local one_prev_updated=$UPDATED one_prev_failed=$FAILED
|
||||
PKGBUILDS_DIR="$one_root" sync_package 1password >/dev/null 2>&1 || true
|
||||
check "1password sync updates without failures" "updated=1 failed=0" \
|
||||
"updated=$((UPDATED - one_prev_updated)) failed=$((FAILED - one_prev_failed))"
|
||||
check "1password pkgver is the single download version source" "$one_version" \
|
||||
"$(grep -m1 '^pkgver=' "$one_dir/PKGBUILD" | cut -d= -f2-)"
|
||||
check "1password pkgrel resets to 1" "1" \
|
||||
"$(grep -m1 '^pkgrel=' "$one_dir/PKGBUILD" | cut -d= -f2-)"
|
||||
check "1password x86 URL follows the rewritten pkgver" "1password-${one_version}.x64.tar.gz" \
|
||||
"$(CARCH=x86_64 bash -c 'source "$1"; basename "${source_x86_64[0]}"' _ "$one_dir/PKGBUILD")"
|
||||
check "1password ARM URL follows the rewritten pkgver" "1password-${one_version}.arm64.tar.gz" \
|
||||
"$(CARCH=aarch64 bash -c 'source "$1"; basename "${source_aarch64[0]}"' _ "$one_dir/PKGBUILD")"
|
||||
|
||||
echo "Quarantine backstop:"
|
||||
local rel st
|
||||
rel=$(jq -n --arg p "$old2d" '{pkgver: "1.9.0", published_at: $p, sha256sums: {}}')
|
||||
@@ -614,10 +873,123 @@ EOF
|
||||
echo '{"source": "local", "upstream": {"github": "example/tool"}}' > "$agepkg/.omarchy/package.json"
|
||||
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
||||
check "upstream without checksums/assets is rejected" "1" "$vst"
|
||||
echo '{"source": "local", "upstream": {"github": "example/tool", "checksums": "SHASUMS256.txt", "digests": true, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json"
|
||||
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
||||
check "upstream with both checksums and digests is rejected" "1" "$vst"
|
||||
echo '{"source": "local", "upstream": {"github": "example/tool", "digests": "yes", "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json"
|
||||
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
||||
check "non-boolean digests is rejected" "1" "$vst"
|
||||
echo '{"source": "local", "upstream": {"github": "example/tool", "checksums": false, "digests": true, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json"
|
||||
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
||||
check "checksums: false alongside digests is rejected" "1" "$vst"
|
||||
echo '{"source": "local", "upstream": {"github": "example/tool", "checksums": "SUMS", "digests": null, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json"
|
||||
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
||||
check "digests: null is rejected" "1" "$vst"
|
||||
echo '{"source": "local", "upstream": {"github": "example/tool", "checksums": "SUMS", "digests": false, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json"
|
||||
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
||||
check "digests: false beside a checksums manifest is accepted" "0" "$vst"
|
||||
echo '{"source": "local", "upstream": {"github": "example/tool", "digests": false, "assets": {"x86_64": "a"}}}' > "$agepkg/.omarchy/package.json"
|
||||
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
||||
check "digests: false alone is rejected" "1" "$vst"
|
||||
cp "$digpkg/.omarchy/package.json" "$agepkg/.omarchy/package.json"
|
||||
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
||||
check "the digests declaration shape is accepted" "0" "$vst"
|
||||
echo '{"source":"local","upstream":{"github":"example/tool","git_tags":"https://example/tool.git","checksums":"sums","assets":{"any":"tool"}}}' > "$agepkg/.omarchy/package.json"
|
||||
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
||||
check "multiple provider types are rejected" "1" "$vst"
|
||||
cp "$pkg/.omarchy/package.json" "$agepkg/.omarchy/package.json"
|
||||
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
||||
check "the real declaration shape is accepted" "0" "$vst"
|
||||
|
||||
echo "Git-tag provider:"
|
||||
local tagpkg="$TEMP_DIR/selftest-tags" tag_sum remote_sum local_sum
|
||||
mkdir -p "$tagpkg/.omarchy"
|
||||
printf 'pkgver=1.0.0\npkgrel=4\nsha256sums=("old" "old")\n' > "$tagpkg/PKGBUILD"
|
||||
printf 'local fixture\n' > "$tagpkg/local.patch"
|
||||
cat > "$tagpkg/.omarchy/package.json" <<'EOF'
|
||||
{
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"git_tags": "https://example.test/tool.git",
|
||||
"tag_pattern": "release/{pkgver}",
|
||||
"sources": {
|
||||
"any": ["https://downloads.example.test/tool-{pkgver}.tar.gz", "file:local.patch"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
git_tags_fetch_refs() {
|
||||
printf '%s\n' \
|
||||
'aaaa refs/tags/release/1.9.0' \
|
||||
'bbbb refs/tags/release/1.10.0' \
|
||||
'cccc refs/tags/not-a-release'
|
||||
}
|
||||
upstream_fetch_source() { printf 'remote fixture for %s\n' "$1" > "$2"; }
|
||||
tag_sum=$(git_tags_upstream_release "$tagpkg")
|
||||
remote_sum=$(printf 'remote fixture for %s\n' 'https://downloads.example.test/tool-1.10.0.tar.gz' | sha256sum | cut -d' ' -f1)
|
||||
local_sum=$(sha256sum "$tagpkg/local.patch" | cut -d' ' -f1)
|
||||
check "pacman ordering selects 1.10.0 over 1.9.0" "1.10.0" "$(jq -r '.pkgver' <<<"$tag_sum")"
|
||||
check "remote source template is downloaded and hashed" "$remote_sum" "$(jq -r '.sha256sums.any[0]' <<<"$tag_sum")"
|
||||
check "local source entry is hashed" "$local_sum" "$(jq -r '.sha256sums.any[1]' <<<"$tag_sum")"
|
||||
vst=0; validate_package_metadata "$tagpkg" >/dev/null || vst=$?
|
||||
check "git-tags declaration validates" "0" "$vst"
|
||||
|
||||
echo "npm provider:"
|
||||
local npmpkg="$TEMP_DIR/selftest-npm" npm_sum npm_tar_sum npm_notes_sum
|
||||
mkdir -p "$npmpkg/.omarchy"
|
||||
printf 'pkgver=1.0.0\npkgrel=1\nsha256sums=("old" "old")\n' > "$npmpkg/PKGBUILD"
|
||||
cat > "$npmpkg/.omarchy/package.json" <<'EOF'
|
||||
{
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"npm": "@example/tool",
|
||||
"dist_tag": "latest",
|
||||
"sources": {
|
||||
"any": ["{npm_tarball}", "https://example.test/tool/{pkgver}/notes"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
npm_fetch_metadata() {
|
||||
jq -n '{
|
||||
"dist-tags": {latest: "2.0.0"},
|
||||
versions: {"2.0.0": {dist: {tarball: "https://registry.npmjs.org/@example/tool/-/tool-2.0.0.tgz"}}},
|
||||
time: {"2.0.0": "2024-01-02T03:04:05.000Z"}
|
||||
}'
|
||||
}
|
||||
npm_sum=$(npm_upstream_release "$npmpkg")
|
||||
npm_tar_sum=$(printf 'remote fixture for %s\n' 'https://registry.npmjs.org/@example/tool/-/tool-2.0.0.tgz' | sha256sum | cut -d' ' -f1)
|
||||
npm_notes_sum=$(printf 'remote fixture for %s\n' 'https://example.test/tool/2.0.0/notes' | sha256sum | cut -d' ' -f1)
|
||||
check "npm dist-tag selects its version" "2.0.0" "$(jq -r '.pkgver' <<<"$npm_sum")"
|
||||
check "npm tarball placeholder is hashed" "$npm_tar_sum" "$(jq -r '.sha256sums.any[0]' <<<"$npm_sum")"
|
||||
check "npm pkgver template is hashed" "$npm_notes_sum" "$(jq -r '.sha256sums.any[1]' <<<"$npm_sum")"
|
||||
check "npm publication time is preserved" "2024-01-02T03:04:05.000Z" "$(jq -r '.published_at' <<<"$npm_sum")"
|
||||
vst=0; validate_package_metadata "$npmpkg" >/dev/null || vst=$?
|
||||
check "npm declaration validates" "0" "$vst"
|
||||
|
||||
# A ")" in a comment inside a checksum array must not end the rewrite early;
|
||||
# voxtype-bin annotates its arrays with "(new in v0.7.5)" and the like.
|
||||
echo "Checksum array rewrite across commented lines:"
|
||||
local commented="$TEMP_DIR/commented-pkgbuild" sum_c=$(printf 'c%.0s' {1..64})
|
||||
cat > "$commented" <<'EOF'
|
||||
sha256sums_x86_64=(
|
||||
# Binaries
|
||||
'aaaa' # tool
|
||||
# Sidecar (new in v0.7.5)
|
||||
'bbbb' # sidecar (x86_64)
|
||||
)
|
||||
sha256sums=( # support files (arch-independent)
|
||||
'dddd'
|
||||
)
|
||||
package() { :; }
|
||||
EOF
|
||||
set_pkgbuild_array "$commented" sha256sums_x86_64 "$sum_c" "$sum_c"
|
||||
set_pkgbuild_array "$commented" sha256sums "$sum_c"
|
||||
check "commented arrays rewrite to valid shell" "0" \
|
||||
"$(bash -n "$commented" 2>/dev/null; echo $?)"
|
||||
check "commented arrays hold only the new checksums" "$sum_c $sum_c|$sum_c|package" \
|
||||
"$(bash -c 'source "$1"; printf "%s|%s|%s" "${sha256sums_x86_64[*]}" "${sha256sums[*]}" "$(declare -F package)"' _ "$commented")"
|
||||
|
||||
# End to end over the real mise-bin package: its checked-in metadata and
|
||||
# PKGBUILD, the full sync_package path (selection, validation, backstop,
|
||||
# rewrite, read-back verification), with only the two network fetches
|
||||
@@ -669,6 +1041,13 @@ EOF
|
||||
check "malformed upstream fails the sync instead of skipping" "1" "$((FAILED - prev_failed))"
|
||||
FAILED=0
|
||||
|
||||
if ! bash "$BUILD_ROOT/pkgbuilds/cua-driver-bin/.omarchy/upstream-test.sh"; then
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
if ! bash "$BUILD_ROOT/pkgbuilds/tmog-bin/.omarchy/upstream-test.sh"; then
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$failures" -eq 0 ]]; then
|
||||
print_success "Self-test passed"
|
||||
@@ -683,16 +1062,100 @@ if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 && "${SPECIFIC_PACKAGES[0]}" == "self-test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Packages that pin the same upstream branch move together or not at all.
|
||||
# The watch reads one shared clone per run, so they only disagree when one
|
||||
# package's update failed after the tip was chosen (a checksum fetch, say).
|
||||
# Leaving the other one advanced would ship omarchy-dev and
|
||||
# omarchy-settings-dev from different commits, which is exactly the skew the
|
||||
# release pair's lockstep guard exists to prevent. Restore the packages that
|
||||
# moved and count the group as failed; the next run tries again.
|
||||
declare -A BEFORE_SYNC=()
|
||||
|
||||
snapshot_package() {
|
||||
local package="$1" pkgbuild="$PKGBUILDS_DIR/$1/PKGBUILD"
|
||||
[[ -f "$pkgbuild" ]] || return 0
|
||||
mkdir -p "$TEMP_DIR/before"
|
||||
cp "$pkgbuild" "$TEMP_DIR/before/$package"
|
||||
BEFORE_SYNC["$package"]=1
|
||||
}
|
||||
|
||||
branch_watch_key() {
|
||||
local package_dir="$1"
|
||||
jq -r '
|
||||
(.upstream.watch? | objects | select(has("git_branch")))
|
||||
| "\(.git_branch)#\(.branch)"
|
||||
' "$package_dir/.omarchy/package.json" 2>/dev/null
|
||||
}
|
||||
|
||||
enforce_branch_lockstep() {
|
||||
local package key
|
||||
declare -A groups=()
|
||||
for package in "${!BEFORE_SYNC[@]}"; do
|
||||
key=$(branch_watch_key "$PKGBUILDS_DIR/$package")
|
||||
[[ -n "$key" ]] || continue
|
||||
groups["$key"]+="$package "
|
||||
done
|
||||
for key in "${!groups[@]}"; do
|
||||
local members commits pin
|
||||
read -r -a members <<<"${groups[$key]}"
|
||||
(( ${#members[@]} > 1 )) || continue
|
||||
commits=$(for package in "${members[@]}"; do
|
||||
pin=$(grep -m1 -E '^_commit=' "$PKGBUILDS_DIR/$package/PKGBUILD" | cut -d= -f2- | tr -d "\"'" || true)
|
||||
printf '%s\n' "${pin:-missing:$package}"
|
||||
done | sort -u | grep -c .)
|
||||
(( commits > 1 )) || continue
|
||||
print_error "Lockstep violation on $key: ${members[*]} pin different commits; restoring all of them"
|
||||
for package in "${members[@]}"; do
|
||||
if ! cmp -s "$TEMP_DIR/before/$package" "$PKGBUILDS_DIR/$package/PKGBUILD"; then
|
||||
cp "$TEMP_DIR/before/$package" "$PKGBUILDS_DIR/$package/PKGBUILD"
|
||||
# Not ((--UPDATED)): an arithmetic command that evaluates to zero
|
||||
# returns 1, and under errexit that would end the run right here.
|
||||
UPDATED=$((UPDATED > 0 ? UPDATED - 1 : 0))
|
||||
fi
|
||||
done
|
||||
((++FAILED))
|
||||
done
|
||||
}
|
||||
|
||||
sync_in_lane() {
|
||||
local package="$1" package_dir="$PKGBUILDS_DIR/$1"
|
||||
snapshot_package "$package"
|
||||
if [[ -d "$package_dir" ]] && ! package_in_lane "$package_dir" "$LANE"; then
|
||||
print_info "Skipping $package: not in the $LANE lane"
|
||||
((++SKIPPED))
|
||||
return 0
|
||||
fi
|
||||
sync_package "$package"
|
||||
}
|
||||
|
||||
if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 ]]; then
|
||||
SPECIFIC_MODE=true
|
||||
# A targeted run is still a branch update: include every sibling watching
|
||||
# the same branch, otherwise the lockstep check never sees the omitted one.
|
||||
declare -A selected=() selected_branches=()
|
||||
for package in "${SPECIFIC_PACKAGES[@]}"; do
|
||||
sync_package "$package"
|
||||
selected["$package"]=1
|
||||
key=$(branch_watch_key "$PKGBUILDS_DIR/$package" || true)
|
||||
[[ -z "$key" ]] || selected_branches["$key"]=1
|
||||
done
|
||||
for package_dir in "$PKGBUILDS_DIR"/*; do
|
||||
[[ -f "$package_dir/PKGBUILD" ]] || continue
|
||||
package=${package_dir##*/}
|
||||
[[ -z "${selected[$package]:-}" ]] || continue
|
||||
key=$(branch_watch_key "$package_dir" || true)
|
||||
if [[ -n "$key" && -n "${selected_branches[$key]:-}" ]]; then
|
||||
SPECIFIC_PACKAGES+=("$package")
|
||||
fi
|
||||
done
|
||||
for package in "${SPECIFIC_PACKAGES[@]}"; do
|
||||
sync_in_lane "$package"
|
||||
done
|
||||
else
|
||||
while IFS= read -r package; do
|
||||
sync_package "$package"
|
||||
sync_in_lane "$package"
|
||||
done < <(packages_for_upstream_sync)
|
||||
fi
|
||||
enforce_branch_lockstep
|
||||
|
||||
echo ""
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
|
||||
+38
-32
@@ -84,15 +84,18 @@ echo ""
|
||||
|
||||
# --- queued work -------------------------------------------------------------
|
||||
|
||||
print_info "Queued builds (state files in $STATE_DIR)"
|
||||
print_info "Queued builds (state files in $STATE_DIR; architectures: $(published_arches | tr '\n' ' '))"
|
||||
queued=false
|
||||
for channel in edge rc stable; do
|
||||
state_file="$STATE_DIR/.sync-needed-$channel"
|
||||
if [[ -f "$state_file" ]]; then
|
||||
queued=true
|
||||
since=$(date -r "$state_file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")
|
||||
printf ' • %-7s queued since %s\n' "$channel" "$since"
|
||||
fi
|
||||
for arch in $(published_arches); do
|
||||
state_file=$(sync_queue_file "$channel" "$arch")
|
||||
[[ "$arch" == "x86_64" && ! -f "$state_file" ]] && state_file=$(legacy_sync_queue_file "$channel")
|
||||
if [[ -f "$state_file" ]]; then
|
||||
queued=true
|
||||
since=$(date -r "$state_file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")
|
||||
printf ' • %-7s %-8s queued since %s\n' "$channel" "$arch" "$since"
|
||||
fi
|
||||
done
|
||||
done
|
||||
[[ "$queued" == false ]] && echo " (nothing queued — all channels up to date)"
|
||||
echo ""
|
||||
@@ -101,34 +104,37 @@ echo ""
|
||||
# say so plainly: it is queued but deliberately not being retried yet.
|
||||
paused=false
|
||||
for channel in edge rc stable; do
|
||||
fail_file="$STATE_DIR/.build-failed-$channel"
|
||||
[[ -f "$fail_file" ]] || continue
|
||||
if [[ "$paused" == false ]]; then
|
||||
print_error "Failing builds (backoff active)"
|
||||
paused=true
|
||||
fi
|
||||
FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT=""
|
||||
# shellcheck disable=SC1090
|
||||
source "$fail_file" 2>/dev/null || true
|
||||
delay=600
|
||||
for ((i = 1; i < FAILURE_COUNT; i++)); do
|
||||
delay=$((delay * 2))
|
||||
((delay >= 21600)) && { delay=21600; break; }
|
||||
for arch in $(published_arches); do
|
||||
fail_file=$(sync_fail_file "$channel" "$arch")
|
||||
[[ "$arch" == "x86_64" && ! -f "$fail_file" ]] && fail_file=$(legacy_sync_fail_file "$channel")
|
||||
[[ -f "$fail_file" ]] || continue
|
||||
if [[ "$paused" == false ]]; then
|
||||
print_error "Failing builds (backoff active)"
|
||||
paused=true
|
||||
fi
|
||||
FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT=""
|
||||
# shellcheck disable=SC1090
|
||||
source "$fail_file" 2>/dev/null || true
|
||||
delay=600
|
||||
for ((i = 1; i < FAILURE_COUNT; i++)); do
|
||||
delay=$((delay * 2))
|
||||
((delay >= 21600)) && { delay=21600; break; }
|
||||
done
|
||||
retry_at=$((FAILURE_AT + delay))
|
||||
now=$(date +%s)
|
||||
if ((now < retry_at)); then
|
||||
when="retries at $(date -d "@$retry_at" '+%H:%M:%S' 2>/dev/null || echo "+$((retry_at - now))s")"
|
||||
else
|
||||
when="retries on the next tick"
|
||||
fi
|
||||
printf ' ✗ %-7s %-8s %s consecutive failure(s), %s\n' "$channel" "$arch" "$FAILURE_COUNT" "$when"
|
||||
printf ' last attempt %s on commit %s\n' \
|
||||
"$(date -d "@$FAILURE_AT" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$FAILURE_AT")" \
|
||||
"${FAILURE_FINGERPRINT:0:12}"
|
||||
done
|
||||
retry_at=$((FAILURE_AT + delay))
|
||||
now=$(date +%s)
|
||||
if ((now < retry_at)); then
|
||||
when="retries at $(date -d "@$retry_at" '+%H:%M:%S' 2>/dev/null || echo "+$((retry_at - now))s")"
|
||||
else
|
||||
when="retries on the next tick"
|
||||
fi
|
||||
printf ' ✗ %-7s %s consecutive failure(s), %s\n' "$channel" "$FAILURE_COUNT" "$when"
|
||||
printf ' last attempt %s on commit %s\n' \
|
||||
"$(date -d "@$FAILURE_AT" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$FAILURE_AT")" \
|
||||
"${FAILURE_FINGERPRINT:0:12}"
|
||||
done
|
||||
if [[ "$paused" == true ]]; then
|
||||
echo " Any new commit clears the backoff; or: rm $STATE_DIR/.build-failed-<channel>"
|
||||
echo " Any new commit clears the backoff; or: rm $STATE_DIR/.build-failed-<channel>-<arch>"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
# Retire packages from one channel of the remote repository.
|
||||
#
|
||||
# unpublish-packages --mirror <edge|rc|stable> --arch <arch> <pkgbase...>
|
||||
#
|
||||
# The counterpart of publish-artifact, with the same steps:
|
||||
# 1. pull the channel's current database from the remote
|
||||
# 2. find every entry built from the named pkgbases (so a package's split
|
||||
# outputs and -debug go with it)
|
||||
# 3. repo-remove them
|
||||
# 4. upload the database
|
||||
#
|
||||
# Package files stay in the bucket. Published filenames are immutable and the
|
||||
# R2 cache cannot recover from a rewrite; an unreferenced file costs nothing
|
||||
# and pacman never sees it. Naming a package the channel does not hold is not
|
||||
# an error, so a re-run is harmless.
|
||||
#
|
||||
# The remote is an rclone remote (REMOTE, default the production one);
|
||||
# OMARCHY_PUBLISH_PREFIX can point a proof run at a scratch prefix.
|
||||
set -euo pipefail
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
|
||||
REMOTE=${REMOTE:-pkgs.omarchy.org:omarchy-pkgs}
|
||||
PREFIX=${OMARCHY_PUBLISH_PREFIX:-}
|
||||
BASES=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--mirror) MIRROR=$2; require_valid_mirror "$MIRROR"; shift 2 ;;
|
||||
--arch) ARCH=$2; require_valid_arch "$ARCH"; shift 2 ;;
|
||||
--remote) REMOTE=$2; shift 2 ;;
|
||||
-h|--help) sed -n '2,19p' "$0"; exit 0 ;;
|
||||
-*) print_error "Unknown option: $1"; exit 1 ;;
|
||||
*) BASES+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
(( ${#BASES[@]} )) || { print_error "No packages given"; exit 1; }
|
||||
|
||||
DEST="$REMOTE/${PREFIX:+$PREFIX/}$MIRROR/$ARCH"
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
print_header "Unpublish from $DEST"
|
||||
|
||||
# --- 1. pull the current database -----------------------------------------
|
||||
mkdir -p "$WORK/repo"
|
||||
listing=$(rclone lsf "$DEST/" --s3-no-head 2>/dev/null || true)
|
||||
if ! grep -q '^omarchy.db.tar.zst$' <<<"$listing"; then
|
||||
print_info "No database at $DEST; nothing to remove"
|
||||
exit 0
|
||||
fi
|
||||
rclone copy "$DEST/omarchy.db.tar.zst" "$WORK/repo" --s3-no-head
|
||||
rclone copy "$DEST/omarchy.files.tar.zst" "$WORK/repo" --s3-no-head 2>/dev/null || true
|
||||
before=$(tar -tf "$WORK/repo/omarchy.db.tar.zst" | grep -c '/$')
|
||||
print_info "Pulled current database ($before entries)"
|
||||
|
||||
# --- 2. entries built from the named pkgbases -----------------------------
|
||||
names=$(bsdtar -xOf "$WORK/repo/omarchy.db.tar.zst" --include '*/desc' |
|
||||
awk -v bases=" ${BASES[*]} " '
|
||||
/^%NAME%$/ { getline name }
|
||||
/^%BASE%$/ { getline base; if (index(bases, " " base " ")) print name }')
|
||||
if [[ -z "$names" ]]; then
|
||||
print_info "None of ${BASES[*]} is in $MIRROR/$ARCH; nothing to remove"
|
||||
exit 0
|
||||
fi
|
||||
mapfile -t NAMES <<<"$names"
|
||||
for n in "${NAMES[@]}"; do print_step "removing $n"; done
|
||||
|
||||
# --- 3. repo-remove ---------------------------------------------------------
|
||||
( cd "$WORK/repo" && repo-remove --quiet omarchy.db.tar.zst "${NAMES[@]}" )
|
||||
ln -sf omarchy.db.tar.zst "$WORK/repo/omarchy.db"
|
||||
ln -sf omarchy.files.tar.zst "$WORK/repo/omarchy.files"
|
||||
after=$(tar -tf "$WORK/repo/omarchy.db.tar.zst" | grep -c '/$')
|
||||
(( before - after == ${#NAMES[@]} )) || {
|
||||
print_error "Expected to remove ${#NAMES[@]} entries, removed $((before - after))"; exit 1; }
|
||||
print_info "Database now has $after entries"
|
||||
|
||||
# --- 4. upload the database -------------------------------------------------
|
||||
rclone copy "$WORK/repo" "$DEST" --s3-no-head --checksum --copy-links --filter '+ omarchy.db*' --filter '+ omarchy.files*' --filter '- *'
|
||||
print_success "Removed ${#NAMES[@]} package(s) from $DEST"
|
||||
+16
-12
@@ -11,12 +11,11 @@ source "$BUILD_ROOT/helpers/docker-helpers.sh"
|
||||
source "$BUILD_ROOT/helpers/paths.sh"
|
||||
source "$BUILD_ROOT/helpers/lock-helpers.sh"
|
||||
|
||||
# Function to update repository database using Docker
|
||||
# Function to update repository database using the selected container engine
|
||||
update_database() {
|
||||
print_info "Updating repository database in Docker container..."
|
||||
print_info "Updating repository database in a container..."
|
||||
|
||||
# Check Docker is available
|
||||
check_docker
|
||||
check_engine
|
||||
|
||||
# Ensure output directory exists
|
||||
if [[ ! -d "$REPO_DIR" ]]; then
|
||||
@@ -25,20 +24,25 @@ update_database() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make output directory writable for container
|
||||
make_dir_writable "$REPO_DIR"
|
||||
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||
make_dir_writable "$REPO_DIR"
|
||||
fi
|
||||
|
||||
# repo-add is architecture- and mirror-independent, so always use the edge
|
||||
# x86_64 image. This also lets bootstrap-rc build the rc database before the
|
||||
# rc channel exists remotely (an rc image can only build after it does).
|
||||
build_docker_image "$BUILD_DIR" "x86_64" "edge"
|
||||
# repo-add is architecture- and mirror-independent. Use the host-native edge
|
||||
# image, which also lets bootstrap-rc run before that channel exists remotely.
|
||||
local tool_arch
|
||||
tool_arch=$(docker_native_arch) || {
|
||||
print_error "Unsupported host architecture: $(uname -m)"
|
||||
exit 1
|
||||
}
|
||||
build_docker_image "$BUILD_DIR" "$tool_arch" "edge"
|
||||
|
||||
docker run --rm --platform linux/amd64 \
|
||||
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$tool_arch")" \
|
||||
-e ARCH="$ARCH" \
|
||||
-e MIRROR="$MIRROR" \
|
||||
-v "$REPO_ROOT:/output" \
|
||||
-v "$BUILD_DIR:/build:ro" \
|
||||
omarchy-pkg-builder:latest-x86_64-edge /build/update-repo.sh
|
||||
"omarchy-pkg-builder:latest-$tool_arch-edge" /build/update-repo.sh
|
||||
}
|
||||
|
||||
# Main execution
|
||||
|
||||
+16
-6
@@ -45,9 +45,7 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then \
|
||||
printf 'Server = https://mirror.omarchy.org/$repo/os/$arch\n' > /etc/pacman.d/mirrorlist; \
|
||||
fi; \
|
||||
else \
|
||||
curl -L "https://raw.githubusercontent.com/archlinuxarm/PKGBUILDs/master/core/pacman-mirrorlist/mirrorlist" 2>/dev/null | \
|
||||
sed -E 's/^\s*#\s*Server\s*=/Server =/g' > /etc/pacman.d/mirrorlist && \
|
||||
sed -i 's/\$arch/aarch64/g' /etc/pacman.d/mirrorlist; \
|
||||
printf 'Server = https://fl.us.mirror.archlinuxarm.org/aarch64/$repo\n' > /etc/pacman.d/mirrorlist; \
|
||||
fi
|
||||
|
||||
# Bootstrap keyrings (required before pacstrap can verify packages)
|
||||
@@ -108,11 +106,16 @@ RUN ln -sf /usr/lib/os-release /etc/os-release && \
|
||||
# Setup Omarchy keyring manually before adding repo (avoids keyserver trust issues)
|
||||
# Note: Repository is removed at the end since build scripts add it dynamically.
|
||||
# The keyring comes from this image's own channel (the bare /$arch path is a
|
||||
# stale legacy layout); %s keeps pacman's $arch literal while MIRROR expands.
|
||||
# stale legacy layout). It is always taken from the x86_64 tree, whatever the
|
||||
# image's own architecture: omarchy-keyring is an arch=any package, and the
|
||||
# x86_64 tree is the one that exists before a new architecture has published
|
||||
# anything. Bootstrapping from the target's own tree would make the first
|
||||
# aarch64 build depend on an aarch64 repository that only that build can
|
||||
# create.
|
||||
ARG MIRROR=edge
|
||||
RUN pacman-key --recv-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 --keyserver keys.openpgp.org && \
|
||||
pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571 && \
|
||||
printf '\n[omarchy]\nSigLevel = Optional\nServer = https://pkgs.omarchy.org/%s/$arch\n' "${MIRROR}" >> /etc/pacman.conf && \
|
||||
printf '\n[omarchy]\nSigLevel = Optional\nServer = https://pkgs.omarchy.org/%s/x86_64\n' "${MIRROR}" >> /etc/pacman.conf && \
|
||||
pacman -Sy --noconfirm && \
|
||||
pacman -S --noconfirm omarchy-keyring && \
|
||||
pacman-key --populate omarchy && \
|
||||
@@ -126,6 +129,7 @@ RUN pacman -Syu --noconfirm && \
|
||||
wget \
|
||||
curl \
|
||||
jq \
|
||||
rclone \
|
||||
gnupg && \
|
||||
pacman -Scc --noconfirm && \
|
||||
rm -rf /var/cache/pacman/pkg/*
|
||||
@@ -137,8 +141,14 @@ RUN useradd -m -G wheel -s /bin/bash builder && \
|
||||
chmod 700 /home/builder/.gnupg && \
|
||||
chown -R builder:builder /home/builder
|
||||
|
||||
# Arch Linux ARM's makepkg.conf still defaults PKGEXT to .pkg.tar.xz; every
|
||||
# tool downstream of the build (sign.sh, push-build, sync-rebuilds, the
|
||||
# notifier) expects .pkg.tar.zst, so an aarch64 package would build and then
|
||||
# be skipped at signing. Pin the extension so both architectures match.
|
||||
RUN sed -i 's/^#MAKEFLAGS=.*/MAKEFLAGS="-j$(nproc)"/' /etc/makepkg.conf && \
|
||||
sed -i 's/^COMPRESSZST=.*/COMPRESSZST=(zstd -c -z -q --threads=0 -)/' /etc/makepkg.conf
|
||||
sed -i 's/^COMPRESSZST=.*/COMPRESSZST=(zstd -c -z -q --threads=0 -)/' /etc/makepkg.conf && \
|
||||
sed -i "s|^PKGEXT=.*|PKGEXT='.pkg.tar.zst'|" /etc/makepkg.conf && \
|
||||
sed -i 's|^#\?PACKAGER=.*|PACKAGER="Omarchy <pkgs@omarchy.org>"|' /etc/makepkg.conf
|
||||
|
||||
# Pacman wrapper that auto-resolves package conflicts (e.g. rustup vs rust).
|
||||
# makepkg only passes --noconfirm to pacman, which defaults to 'N' on conflict
|
||||
|
||||
+250
-149
@@ -1,21 +1,88 @@
|
||||
#!/bin/bash
|
||||
# Build script - builds packages based on package metadata
|
||||
# Plan a run or build one planned package in an isolated container.
|
||||
# Unscoped edge builds exclude skip_build packages. Stable also requires the fast release ring.
|
||||
# Explicit --package selections may build packages with skip_build=true.
|
||||
|
||||
# Setup directories
|
||||
ARCH=${ARCH:-x86_64}
|
||||
# ARCH selects the repository target for this script, but make and Kbuild also
|
||||
# interpret an exported ARCH themselves (Linux calls this target "arm64").
|
||||
# Keep the shell variable local to the orchestrator so PKGBUILDs see CARCH only.
|
||||
export -n ARCH
|
||||
MIRROR=${MIRROR:-edge}
|
||||
DRY_RUN=${DRY_RUN:-false}
|
||||
# bin/build plans the whole run, then invokes this script once per package in
|
||||
# a fresh container. PACKAGES retains the original request for validation.
|
||||
BUILD_PACKAGE=${BUILD_PACKAGE:-}
|
||||
BUILD_PLAN_DIR=${BUILD_PLAN_DIR:-}
|
||||
PKGBUILDS_DIR=${PKGBUILDS_DIR:-/pkgbuilds}
|
||||
BUILD_OUTPUT_DIR=${BUILD_OUTPUT_DIR:-/build-output/$MIRROR/$ARCH}
|
||||
FINAL_OUTPUT_DIR=${FINAL_OUTPUT_DIR:-/pkgs.omarchy.org/$MIRROR/$ARCH}
|
||||
HELPERS_DIR=${HELPERS_DIR:-/helpers}
|
||||
SRC_DIR=${SRC_DIR:-/src}
|
||||
# Set by bin/build from OMARCHY_DEFER_RUNTIME_DEPS after it has checked the
|
||||
# request; re-checked here so the container never trusts a stray value.
|
||||
DEFER_RUNTIME_DEPS=${DEFER_RUNTIME_DEPS:-false}
|
||||
|
||||
source "$HELPERS_DIR/package-metadata.sh"
|
||||
|
||||
# Where the channel's published database is read from for planning. On the
|
||||
# repository host it is the published tree itself. Anywhere else (a CI runner,
|
||||
# a fresh clone) that tree is absent, so the database is fetched from the
|
||||
# public channel and the same URL serves as pacman's dependency repository.
|
||||
# Set OMARCHY_PUBLISHED_REPO_URL= (empty) to disable the remote fallback.
|
||||
PUBLISHED_REPO_URL=${OMARCHY_PUBLISHED_REPO_URL-https://pkgs.omarchy.org}
|
||||
PUBLISHED_DB_DIR="$FINAL_OUTPUT_DIR"
|
||||
PUBLISHED_REPO_SERVER=""
|
||||
if [[ ! -f "$FINAL_OUTPUT_DIR/omarchy.db.tar.zst" && ! -f "$FINAL_OUTPUT_DIR/omarchy.db" && -n "$PUBLISHED_REPO_URL" ]]; then
|
||||
remote_channel="$PUBLISHED_REPO_URL/$MIRROR/$ARCH"
|
||||
remote_db_dir=$(mktemp -d /tmp/omarchy-published.XXXXXX) || exit 1
|
||||
# Cache-bust: the channel sits behind a CDN that serves a stale database
|
||||
# for a while after a sync.
|
||||
if curl -fsSL "$remote_channel/omarchy.db.tar.zst?$(date +%s)" -o "$remote_db_dir/omarchy.db.tar.zst"; then
|
||||
PUBLISHED_DB_DIR="$remote_db_dir"
|
||||
PUBLISHED_REPO_SERVER="$remote_channel"
|
||||
echo "==> No local published tree; planning against $remote_channel"
|
||||
else
|
||||
rm -rf "$remote_db_dir"
|
||||
echo "==> No local published tree and $remote_channel is unavailable; treating the channel as empty"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $DEFER_RUNTIME_DEPS != "false" && $DEFER_RUNTIME_DEPS != "true" ]]; then
|
||||
echo "DEFER_RUNTIME_DEPS must be true or false" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ $DEFER_RUNTIME_DEPS == "true" ]]; then
|
||||
deferred_runtime=0
|
||||
deferred_settings=0
|
||||
deferred_count=0
|
||||
for package in $PACKAGES; do
|
||||
((deferred_count += 1))
|
||||
case $package in
|
||||
omarchy|omarchy-dev) deferred_runtime=1 ;;
|
||||
omarchy-settings|omarchy-settings-dev) deferred_settings=1 ;;
|
||||
*)
|
||||
echo "Runtime dependency deferral only applies to the omarchy pair, not $package" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if (( deferred_runtime != 1 || deferred_settings != 1 || deferred_count != 2 )); then
|
||||
echo "Runtime dependency deferral requires exactly the omarchy pair" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" != true ]]; then
|
||||
if [[ -z "$BUILD_PACKAGE" || -z "$BUILD_PLAN_DIR" ]]; then
|
||||
echo "Use bin/build to plan and run isolated package builds" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -Fxq -- "$BUILD_PACKAGE" "$BUILD_PLAN_DIR/packages"; then
|
||||
echo "Package is not in the build plan: $BUILD_PACKAGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Import GPG keys
|
||||
/build/import-gpg-keys.sh || exit 1
|
||||
|
||||
@@ -27,49 +94,66 @@ if [[ "$DRY_RUN" != true ]]; then
|
||||
# that breaks the new packages (imagemagick wanting GLIBC_2.44, etc).
|
||||
# Done before the Omarchy repos are added so only core/extra participate.
|
||||
echo "==> Updating build container packages..."
|
||||
sudo pacman -Syu --noconfirm
|
||||
sudo pacman -Syu --noconfirm || exit 1
|
||||
|
||||
# Configure Omarchy repositories for dependency resolution
|
||||
echo "==> Configuring Omarchy repositories for dependency resolution..."
|
||||
|
||||
# Always add omarchy-build repo (for incremental builds)
|
||||
# Packages in build-output are unsigned, so use SigLevel = Never
|
||||
sudo tee -a /etc/pacman.conf > /dev/null <<EOF
|
||||
|
||||
[omarchy-build]
|
||||
SigLevel = Never
|
||||
Server = file://$BUILD_OUTPUT_DIR
|
||||
EOF
|
||||
# Always add omarchy-build repo first (for incremental builds). Repository
|
||||
# order is pacman's priority order, so this must precede the official repos;
|
||||
# otherwise pacman can select an older official package with the same name.
|
||||
# Packages in build-output are unsigned, so use SigLevel = Never.
|
||||
sudo sed -i "/^\[core\]$/i [omarchy-build]\nSigLevel = Never\nServer = file://$BUILD_OUTPUT_DIR\n" /etc/pacman.conf
|
||||
echo " -> omarchy-build (priority 1): $BUILD_OUTPUT_DIR"
|
||||
|
||||
# Initialize empty build database if it doesn't exist
|
||||
cd "$BUILD_OUTPUT_DIR"
|
||||
cd "$BUILD_OUTPUT_DIR" || exit 1
|
||||
if [[ ! -f "omarchy-build.db.tar.zst" ]]; then
|
||||
# Create an empty database
|
||||
repo-add omarchy-build.db.tar.zst >/dev/null 2>&1
|
||||
ln -sf omarchy-build.db.tar.zst omarchy-build.db
|
||||
else
|
||||
# Database exists, check if we need to rebuild it from packages
|
||||
if ls *.pkg.tar.* 2>/dev/null | grep -v '\.sig$' | grep -v 'omarchy-build\.db' | grep -q .; then
|
||||
echo "==> Rebuilding build database from existing packages..."
|
||||
ls *.pkg.tar.* | grep -v '\.sig$' | grep -v 'omarchy-build\.db' | xargs -r repo-add omarchy-build.db.tar.zst >/dev/null 2>&1
|
||||
ln -sf omarchy-build.db.tar.zst omarchy-build.db
|
||||
fi
|
||||
repo-add omarchy-build.db.tar.zst >/dev/null 2>&1 || exit 1
|
||||
ln -sf omarchy-build.db.tar.zst omarchy-build.db || exit 1
|
||||
fi
|
||||
# Fold any packages already in the workspace into the database, whether
|
||||
# they came with an existing database or were dropped in by an earlier
|
||||
# workflow job (OMARCHY_KEEP_BUILD_WORKSPACE). Without this a seeded
|
||||
# workspace with no database would leave those packages invisible to
|
||||
# dependency resolution.
|
||||
staged_packages=()
|
||||
for staged in *.pkg.tar.*; do
|
||||
[[ -f "$staged" && "$staged" != *.sig ]] || continue
|
||||
staged_packages+=("$staged")
|
||||
done
|
||||
if (( ${#staged_packages[@]} )); then
|
||||
# Seed once per run. After that, only successful builds update the DB;
|
||||
# rescanning in every container could reintroduce a failed build's partial
|
||||
# outputs or overwrite the new version with an older kept artifact.
|
||||
if [[ ! -e "$BUILD_PLAN_DIR/repository-initialized" ]]; then
|
||||
echo "==> Rebuilding build database from existing packages..."
|
||||
repo-add omarchy-build.db.tar.zst "${staged_packages[@]}" >/dev/null 2>&1 || exit 1
|
||||
ln -sf omarchy-build.db.tar.zst omarchy-build.db || exit 1
|
||||
fi
|
||||
# A resumed/repeated build can produce different bytes under the same
|
||||
# filename. Never let the shared download cache substitute older bytes
|
||||
# for the staged artifacts described by this run's database.
|
||||
for staged in "${staged_packages[@]}"; do
|
||||
sudo rm -f "/var/cache/pacman/pkg/$staged" "/var/cache/pacman/pkg/$staged.sig" || exit 1
|
||||
done
|
||||
fi
|
||||
touch "$BUILD_PLAN_DIR/repository-initialized" || exit 1
|
||||
|
||||
# Add omarchy repo if it has a database (stable packages)
|
||||
# Add omarchy repo if it has a database (stable packages). The local tree
|
||||
# is trusted as-is; the public channel is verified against the omarchy
|
||||
# keyring the image already carries.
|
||||
if [[ -f "$FINAL_OUTPUT_DIR/omarchy.db.tar.zst" ]] || [[ -f "$FINAL_OUTPUT_DIR/omarchy.db" ]]; then
|
||||
sudo tee -a /etc/pacman.conf > /dev/null <<EOF
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = file://$FINAL_OUTPUT_DIR
|
||||
EOF
|
||||
sudo sed -i "/^\[core\]$/i [omarchy]\nSigLevel = Optional TrustAll\nServer = file://$FINAL_OUTPUT_DIR\n" /etc/pacman.conf
|
||||
echo " -> omarchy (priority 2): $FINAL_OUTPUT_DIR"
|
||||
elif [[ -n "$PUBLISHED_REPO_SERVER" ]]; then
|
||||
sudo sed -i "/^\[core\]$/i [omarchy]\nSigLevel = Required DatabaseOptional\nServer = $PUBLISHED_REPO_SERVER\n" /etc/pacman.conf
|
||||
echo " -> omarchy (priority 2): $PUBLISHED_REPO_SERVER"
|
||||
fi
|
||||
|
||||
# Sync pacman database
|
||||
sudo pacman -Sy
|
||||
sudo pacman -Sy || exit 1
|
||||
fi
|
||||
|
||||
echo "==> Package Builder"
|
||||
@@ -82,8 +166,6 @@ if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "==> Dry run: yes (plan only; makepkg will not run)"
|
||||
fi
|
||||
|
||||
FAILED_PACKAGES=""
|
||||
SUCCESSFUL_PACKAGES=""
|
||||
SKIPPED_PACKAGES=""
|
||||
|
||||
# Find package directory
|
||||
@@ -105,10 +187,10 @@ LOCAL_VERSION_CACHE_LOADED=false
|
||||
LOCAL_VERSION_CACHE_DB=""
|
||||
|
||||
load_local_versions() {
|
||||
local db="$FINAL_OUTPUT_DIR/omarchy.db.tar.zst"
|
||||
local db="$PUBLISHED_DB_DIR/omarchy.db.tar.zst"
|
||||
|
||||
if [[ ! -f "$db" ]]; then
|
||||
db="$FINAL_OUTPUT_DIR/omarchy.db"
|
||||
db="$PUBLISHED_DB_DIR/omarchy.db"
|
||||
fi
|
||||
|
||||
[[ -f "$db" ]] || return 0
|
||||
@@ -155,26 +237,9 @@ get_local_version() {
|
||||
# Returns 0 (success) if should build, 1 if should skip
|
||||
should_build_for_arch() {
|
||||
local pkg="$1"
|
||||
local current_arch="$ARCH"
|
||||
local pkgdir=$(find_package_dir "$pkg")
|
||||
local pkgbuild="$pkgdir/PKGBUILD"
|
||||
|
||||
[[ ! -f "$pkgbuild" ]] && return 1
|
||||
|
||||
# Check PKGBUILD arch=() array
|
||||
local pkgbuild_archs=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${arch[@]}"')
|
||||
|
||||
# If arch=('any'), build for all architectures
|
||||
if [[ "$pkgbuild_archs" == "any" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if current arch is in PKGBUILD arch=()
|
||||
if echo "$pkgbuild_archs" | grep -qw "$current_arch"; then
|
||||
return 0 # Build
|
||||
else
|
||||
return 1 # Skip
|
||||
fi
|
||||
local pkgdir
|
||||
pkgdir=$(find_package_dir "$pkg")
|
||||
[[ -n "$pkgdir" ]] && package_supports_arch "$pkgdir" "$ARCH"
|
||||
}
|
||||
|
||||
# For VCS packages, makepkg recalculates pkgver() before the build. If the
|
||||
@@ -224,22 +289,62 @@ refresh_vcs_pkgver_preserving_local_pkgrel() {
|
||||
fi
|
||||
}
|
||||
|
||||
# With runtime dependency checks deferred, makepkg runs --nodeps, so the
|
||||
# build-time dependencies it would normally install with -s have to be
|
||||
# installed explicitly: makedepends and checkdepends, including the
|
||||
# architecture-suffixed variants for the current CARCH.
|
||||
install_deferred_build_dependencies() {
|
||||
local pkg="$1"
|
||||
local -a build_deps=()
|
||||
|
||||
mapfile -t build_deps < <(
|
||||
CARCH="$ARCH" bash -c '
|
||||
source PKGBUILD
|
||||
arch_makedepends="makedepends_${CARCH}[@]"
|
||||
arch_checkdepends="checkdepends_${CARCH}[@]"
|
||||
printf "%s\n" \
|
||||
"${makedepends[@]}" "${!arch_makedepends}" \
|
||||
"${checkdepends[@]}" "${!arch_checkdepends}"
|
||||
' | awk 'NF && !seen[$0]++'
|
||||
)
|
||||
|
||||
if (( ${#build_deps[@]} )); then
|
||||
echo " Installing build-only dependencies for $pkg..."
|
||||
sudo pacman -S --needed --noconfirm -- "${build_deps[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Build a package
|
||||
build_package() {
|
||||
local pkg="$1"
|
||||
local pkgdir=$(find_package_dir "$pkg")
|
||||
local pkgdir
|
||||
pkgdir=$(find_package_dir "$pkg") || return 1
|
||||
|
||||
echo ""
|
||||
echo " -> Processing: $pkg"
|
||||
|
||||
# Install this consumer's freshly built prerequisites in its own container.
|
||||
# Qualifying the repository also upgrades an older dependency baked into
|
||||
# the base image, even if that version would satisfy makepkg's check.
|
||||
if [[ "$DEFER_RUNTIME_DEPS" != true ]]; then
|
||||
local consumer dependency
|
||||
local -a built_deps=()
|
||||
while read -r consumer dependency; do
|
||||
[[ "$consumer" == "$pkg" ]] && built_deps+=("omarchy-build/$dependency")
|
||||
done < "$BUILD_PLAN_DIR/dependencies"
|
||||
if (( ${#built_deps[@]} )); then
|
||||
echo " Installing freshly built dependencies for $pkg..."
|
||||
sudo /usr/local/bin/pacman-for-makepkg -S --needed --noconfirm -- "${built_deps[@]}" || return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Copy to build directory
|
||||
cd /src
|
||||
rm -rf "$pkg"
|
||||
cp -r "$pkgdir" "$pkg"
|
||||
cd /src || return 1
|
||||
rm -rf "$pkg" || return 1
|
||||
cp -r "$pkgdir" "$pkg" || return 1
|
||||
cd "/src/$pkg" || return 1
|
||||
|
||||
refresh_vcs_pkgver_preserving_local_pkgrel "$pkg" || {
|
||||
FAILED_PACKAGES="$FAILED_PACKAGES $pkg"
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -248,7 +353,6 @@ build_package() {
|
||||
|
||||
if [[ -z "$pkgbuild_version" ]]; then
|
||||
echo " Failed to read PKGBUILD version"
|
||||
FAILED_PACKAGES="$FAILED_PACKAGES $pkg"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -280,37 +384,72 @@ build_package() {
|
||||
# Build package without signing (signing is done separately)
|
||||
# PACMAN override uses a wrapper that adds --ask 4 to auto-resolve conflicts
|
||||
# (e.g. rustup replacing rust) since --noconfirm defaults to 'N' on those prompts
|
||||
MAKEPKG_FLAGS="-scf --noconfirm"
|
||||
local -a makepkg_flags=(-scf --noconfirm)
|
||||
if [[ $DEFER_RUNTIME_DEPS == "true" ]]; then
|
||||
# The pair's runtime dependencies (each other, and packages other jobs
|
||||
# of the same pipeline build) are not resolvable here; the assembled set
|
||||
# is installed in one verified transaction downstream. Only the
|
||||
# build-time dependencies are installed, then makepkg skips the check.
|
||||
install_deferred_build_dependencies "$pkg" || {
|
||||
return 1
|
||||
}
|
||||
makepkg_flags=(-cf --noconfirm --nodeps)
|
||||
fi
|
||||
|
||||
if PACMAN=/usr/local/bin/pacman-for-makepkg makepkg $MAKEPKG_FLAGS; then
|
||||
if PACMAN=/usr/local/bin/pacman-for-makepkg makepkg "${makepkg_flags[@]}"; then
|
||||
# Ensure output directory exists
|
||||
mkdir -p "$BUILD_OUTPUT_DIR"
|
||||
|
||||
for pkg_file in *.pkg.tar.*; do
|
||||
[[ -f "$pkg_file" ]] && cp "$pkg_file" "$BUILD_OUTPUT_DIR/"
|
||||
done
|
||||
|
||||
cd "$BUILD_OUTPUT_DIR"
|
||||
# Copy only the artifacts makepkg declares as outputs. A PKGBUILD may use
|
||||
# another pacman package as a source (schist-bin does); a *.pkg.tar.* glob
|
||||
# would mistake that source archive for one of our freshly built packages.
|
||||
local -a package_files=()
|
||||
mapfile -t package_files < <(makepkg --packagelist)
|
||||
|
||||
# Find ALL package files (handles split packages)
|
||||
local new_pkgs=($(ls -t ${pkg}-*.pkg.tar.* 2>/dev/null | grep -v '\.sig$' | grep -v 'omarchy-build\.db'))
|
||||
|
||||
if [[ ${#new_pkgs[@]} -gt 0 ]]; then
|
||||
repo-add omarchy-build.db.tar.zst "${new_pkgs[@]}" >/dev/null 2>&1
|
||||
ln -sf omarchy-build.db.tar.zst omarchy-build.db
|
||||
sudo pacman -Sy >/dev/null 2>&1
|
||||
if [[ ${#package_files[@]} -eq 0 ]]; then
|
||||
echo " Makepkg produced no package files for $pkg"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cd /src/$pkg
|
||||
local -a new_pkgs=()
|
||||
local pkg_path pkg_file
|
||||
for pkg_path in "${package_files[@]}"; do
|
||||
pkg_file=${pkg_path##*/}
|
||||
if [[ ! -f "$pkg_file" ]]; then
|
||||
# makepkg predicts an automatic -debug output whenever debug is
|
||||
# enabled, but data-only packages may contain no symbols and therefore
|
||||
# legitimately produce no debug archive.
|
||||
if [[ "$pkg_file" == *-debug-*.pkg.tar.* ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " Expected package file was not produced: $pkg_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cp "$pkg_file" "$BUILD_OUTPUT_DIR/" || return 1
|
||||
new_pkgs+=("$pkg_file")
|
||||
done
|
||||
|
||||
cd "$BUILD_OUTPUT_DIR" || return 1
|
||||
|
||||
# Add every output from this build, including split packages.
|
||||
if [[ ${#new_pkgs[@]} -gt 0 ]]; then
|
||||
repo-add omarchy-build.db.tar.zst "${new_pkgs[@]}" >/dev/null 2>&1 || return 1
|
||||
ln -sf omarchy-build.db.tar.zst omarchy-build.db || return 1
|
||||
fi
|
||||
|
||||
# A release may publish successful builds even when a peer fails. Record
|
||||
# outputs only after this package's entire split build has completed.
|
||||
mkdir -p "$BUILD_PLAN_DIR/artifacts" || return 1
|
||||
printf '%s\n' "${new_pkgs[@]}" > "$BUILD_PLAN_DIR/artifacts/$pkg" || return 1
|
||||
|
||||
echo " Successfully built $pkg"
|
||||
SUCCESSFUL_PACKAGES="$SUCCESSFUL_PACKAGES $pkg"
|
||||
return 0
|
||||
else
|
||||
echo " Makepkg failed for $pkg"
|
||||
echo " DEBUG: Files in build directory:"
|
||||
ls -lah *.pkg.tar.* 2>&1 | head -20 || echo " No package files found"
|
||||
FAILED_PACKAGES="$FAILED_PACKAGES $pkg"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -325,11 +464,17 @@ get_package_deps() {
|
||||
return
|
||||
fi
|
||||
|
||||
# Extract depends and makedepends, filter for packages in our pkgbuilds/
|
||||
# Include test dependencies and target-specific arrays: each container must
|
||||
# receive its prerequisites through the repository, not a previous build.
|
||||
(
|
||||
CARCH="$ARCH"
|
||||
source "$pkgbuild" 2>/dev/null
|
||||
echo "${depends[@]} ${makedepends[@]}"
|
||||
) | tr ' ' '\n' | while read -r dep; do
|
||||
for kind in depends makedepends checkdepends; do
|
||||
generic="${kind}[@]"
|
||||
specific="${kind}_${CARCH}[@]"
|
||||
printf '%s\n' "${!generic}" "${!specific}"
|
||||
done
|
||||
) | awk 'NF && !seen[$0]++' | while read -r dep; do
|
||||
# Strip version constraints (e.g., 'hyprshade>=1.0' -> 'hyprshade')
|
||||
dep=$(echo "$dep" | sed 's/[<>=].*$//')
|
||||
# Check if this dependency exists in our pkgbuilds
|
||||
@@ -414,27 +559,37 @@ check_needs_build() {
|
||||
|
||||
if [[ "$local_version" == "$pkgbuild_version" ]]; then
|
||||
return 1 # Already up to date
|
||||
else
|
||||
return 0 # Needs building
|
||||
fi
|
||||
|
||||
# Match check-versions: a retained archive is already published even when
|
||||
# the DB now indexes a newer release (for example, 4.0.4rc1 vs 4.0.3).
|
||||
# Rebuilding it would produce different bytes under an immutable filename.
|
||||
if package_version_is_published "$FINAL_OUTPUT_DIR" "$pkg" "$pkgbuild_version" "$ARCH"; then
|
||||
echo " + $pkg $pkgbuild_version - archive already published; skipping rebuild"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0 # Needs building
|
||||
}
|
||||
|
||||
# Collect packages that should be built for the selected mirror
|
||||
collect_packages() {
|
||||
packages_for_unscoped_build "$MIRROR"
|
||||
packages_for_unscoped_build "$MIRROR" "$ARCH"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
if [[ "$DRY_RUN" != true ]]; then
|
||||
cd "$SRC_DIR"
|
||||
cd "$SRC_DIR" || exit 1
|
||||
build_package "$BUILD_PACKAGE"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
TOTAL_COUNT=0
|
||||
|
||||
echo "==> Checking which packages need building..."
|
||||
|
||||
# First pass: determine which packages need building
|
||||
PACKAGES_TO_BUILD=()
|
||||
ORDERED_PACKAGES=()
|
||||
PLANNED_DEPENDENCIES=()
|
||||
|
||||
# If PACKAGES is specified, only check those packages
|
||||
if [[ -n "$PACKAGES" ]]; then
|
||||
@@ -473,13 +628,6 @@ if [[ -n "$PACKAGES" ]]; then
|
||||
else
|
||||
# Build all packages that need updates from the relevant directories
|
||||
while IFS= read -r pkg; do
|
||||
# Check if package should be built for this architecture
|
||||
if ! should_build_for_arch "$pkg"; then
|
||||
echo " - $pkg - not built for $ARCH"
|
||||
SKIPPED_PACKAGES="$SKIPPED_PACKAGES $pkg"
|
||||
continue
|
||||
fi
|
||||
|
||||
if check_needs_build "$pkg"; then
|
||||
PACKAGES_TO_BUILD+=("$pkg")
|
||||
else
|
||||
@@ -492,7 +640,7 @@ fi
|
||||
if [[ ${#PACKAGES_TO_BUILD[@]} -eq 0 ]]; then
|
||||
echo "==> All packages are up to date!"
|
||||
else
|
||||
echo "==> ${#PACKAGES_TO_BUILD[@]} package(s) need building: ${PACKAGES_TO_BUILD[@]}"
|
||||
echo "==> ${#PACKAGES_TO_BUILD[@]} package(s) need building: ${PACKAGES_TO_BUILD[*]}"
|
||||
echo "==> Determining build order based on dependencies..."
|
||||
|
||||
# Second pass: order only the packages that need building
|
||||
@@ -515,6 +663,7 @@ else
|
||||
((unmet_deps_count[$pkg]++))
|
||||
# Track that dep blocks pkg from building
|
||||
blocks_packages[$dep]="${blocks_packages[$dep]} $pkg"
|
||||
PLANNED_DEPENDENCIES+=("$pkg $dep")
|
||||
fi
|
||||
done
|
||||
done < <(get_package_deps "$pkg")
|
||||
@@ -529,7 +678,6 @@ else
|
||||
done
|
||||
|
||||
# Build packages as dependencies become available
|
||||
ORDERED_PACKAGES=()
|
||||
while [[ ${#ready_to_build[@]} -gt 0 ]]; do
|
||||
# Take the first ready package
|
||||
current="${ready_to_build[0]}"
|
||||
@@ -551,63 +699,16 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Build order: ${ORDERED_PACKAGES[@]}"
|
||||
echo "==> Build order: ${ORDERED_PACKAGES[*]}"
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo ""
|
||||
echo "==> Dry run complete. Packages that would build: ${ORDERED_PACKAGES[@]}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Determine which packages need to be installed for other packages being built
|
||||
declare -A INSTALL_PACKAGES
|
||||
for pkg in "${ORDERED_PACKAGES[@]}"; do
|
||||
while IFS= read -r dep; do
|
||||
[[ -z "$dep" ]] && continue
|
||||
# Only install if it's being built in this run
|
||||
for build_pkg in "${ORDERED_PACKAGES[@]}"; do
|
||||
[[ "$dep" == "$build_pkg" ]] && INSTALL_PACKAGES["$dep"]=1
|
||||
done
|
||||
done < <(get_package_deps "$pkg")
|
||||
done
|
||||
|
||||
if [[ ${#INSTALL_PACKAGES[@]} -gt 0 ]]; then
|
||||
echo "==> Packages needed as dependencies: ${!INSTALL_PACKAGES[@]}"
|
||||
fi
|
||||
|
||||
# Build packages in dependency order
|
||||
for pkg in "${ORDERED_PACKAGES[@]}"; do
|
||||
((TOTAL_COUNT++))
|
||||
build_package "$pkg"
|
||||
done
|
||||
# The host consumes plain data, never shell code or parsed human log output.
|
||||
if [[ -n "$BUILD_PLAN_DIR" ]]; then
|
||||
mkdir -p "$BUILD_PLAN_DIR" || exit 1
|
||||
printf '%s\n' "${ORDERED_PACKAGES[@]}" | sed '/^$/d' > "$BUILD_PLAN_DIR/packages" || exit 1
|
||||
printf '%s\n' "${PLANNED_DEPENDENCIES[@]}" | sed '/^$/d' > "$BUILD_PLAN_DIR/dependencies" || exit 1
|
||||
printf '%s\n' $SKIPPED_PACKAGES | sed '/^$/d' > "$BUILD_PLAN_DIR/skipped" || exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "==> Build Summary"
|
||||
echo "========================================"
|
||||
|
||||
# Count results
|
||||
SUCCESS_COUNT=$(echo $SUCCESSFUL_PACKAGES | wc -w)
|
||||
SKIPPED_COUNT=$(echo $SKIPPED_PACKAGES | wc -w)
|
||||
FAILED_COUNT=$(echo $FAILED_PACKAGES | wc -w)
|
||||
|
||||
echo " Total packages: $TOTAL_COUNT"
|
||||
echo " Built: $SUCCESS_COUNT"
|
||||
echo " Skipped: $SKIPPED_COUNT (already up-to-date)"
|
||||
echo " Failed: $FAILED_COUNT"
|
||||
|
||||
# List failures if any
|
||||
if [[ -n "$FAILED_PACKAGES" ]]; then
|
||||
echo ""
|
||||
echo "Failed packages:"
|
||||
for pkg in $FAILED_PACKAGES; do
|
||||
echo " - $pkg"
|
||||
done
|
||||
echo ""
|
||||
echo "==> Some packages failed to build"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> All packages processed successfully!"
|
||||
echo "==> Plan complete. Packages that would build: ${ORDERED_PACKAGES[*]}"
|
||||
@@ -0,0 +1,96 @@
|
||||
# CI spike: build PRs on ephemeral DigitalOcean droplets
|
||||
|
||||
Status: spike. Nothing here publishes. The repository host keeps building and
|
||||
signing on merge exactly as before.
|
||||
|
||||
## Pieces
|
||||
|
||||
- `.github/workflows/build-pr.yml` — on a PR touching `pkgbuilds/**`, one job
|
||||
per changed package on runners labelled `omarchy-builder`. aarch64 jobs
|
||||
run on GitHub's native `ubuntu-24.04-arm` runners instead. Uploads the unsigned
|
||||
`.pkg.tar.zst` as a workflow artifact (7 days).
|
||||
- `runner-cloud-init.yaml` — Ubuntu 24.04 user-data: docker + buildx, the
|
||||
GitHub runner registered `--ephemeral`, runs one job, powers off.
|
||||
- `controller.sh` — systemd timer every minute on a small always-on droplet.
|
||||
Polls for queued jobs with our label, creates one g5-32vcpu-64gb-50gb droplet per job up
|
||||
to `MAX_DROPLETS`, deletes droplets that are powered off or older than
|
||||
`MAX_AGE_MINUTES`, or still provisioning after `MAX_BOOT_MINUTES`. Builders go in any region DigitalOcean lists the size in
|
||||
stock in (`REGIONS` only sets which to try first); a refused create, logged
|
||||
with DigitalOcean's message, falls back to the next region, then the next
|
||||
of `SIZES`. No inbound endpoint. Plain curl against both APIs, no
|
||||
doctl and no gh: a token in the environment cannot pick the wrong account
|
||||
the way a saved doctl context can. Needs curl and jq.
|
||||
`tests/controller.sh` exercises every decision against canned responses.
|
||||
- `controller-box/` — the always-on droplet: unit, timer, env template,
|
||||
cloud-init, and `create.sh` to stand it up with one API call.
|
||||
|
||||
## Standing up the controller box
|
||||
|
||||
DIGITALOCEAN_TOKEN=<omarchy account> GITHUB_TOKEN=<fine-grained PAT> \
|
||||
REPO=omacom/omarchy-pkgs ci/controller-box/create.sh <branch>
|
||||
|
||||
The GitHub PAT is fine-grained, scoped to the one repo: Actions read,
|
||||
Administration read+write (registration tokens). The DO token is baked into
|
||||
the box's env file, so it is the account that pays for builder droplets.
|
||||
Watch it with `journalctl -u omarchy-controller -f` on the box.
|
||||
|
||||
Each tick pulls the box's checkout first, so a merged `controller.sh` is live
|
||||
within a minute. The unit and timer are copies made at creation; after
|
||||
changing them, on the box:
|
||||
|
||||
cp /opt/omarchy-pkgs/ci/controller-box/omarchy-controller.{service,timer} /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
|
||||
## What the spike proved (2026-09-17, fork ryanrhughes/omarchy-pkgs)
|
||||
|
||||
- `bin/build` works from a bare clone: with no local published tree it
|
||||
plans against and resolves from `https://pkgs.omarchy.org/<mirror>/<arch>`.
|
||||
- Droplet create → runner registered: ~70 s. omarchy-fish PR job: 2 min
|
||||
including the builder image build. Droplet powers off after the job.
|
||||
- linux-omarchy on a c-32 droplet: 30 min wall clock for the build job
|
||||
(23:39 → 00:09), 254 MB artifact. Cold start ~90 s before the job began.
|
||||
- A PR whose PKGBUILD fails to build turns the required check red and GitHub
|
||||
refuses the merge (`mergeStateStatus=BLOCKED`, `gh pr merge` refuses
|
||||
without `--admin`).
|
||||
- Controller: one queued job + one busy droplet ⇒ creates exactly one more;
|
||||
reaps powered-off droplets on the next tick.
|
||||
|
||||
## Not done (required before this touches the real repo)
|
||||
|
||||
- Tooling from base: check out master's `bin/ helpers/ build/` and overlay
|
||||
only the PR's `pkgbuilds/<name>`; today a PR can edit the build script
|
||||
and it runs on the droplet. The vouch gate limits who can do that, not
|
||||
what they can do.
|
||||
- DigitalOcean cloud firewall on the `omarchy-builder` tag: no inbound, no
|
||||
egress to private ranges or the metadata address.
|
||||
- A fine-grained GitHub token for the real repository (the one on the
|
||||
controller box is scoped to the fork), and the publish environment's
|
||||
secrets set there.
|
||||
- Disable the host's auto-release timers for any channel CI publishes to,
|
||||
so two writers never touch one database.
|
||||
|
||||
## Done since the spike README was first written
|
||||
|
||||
- Controller as a systemd timer on its own droplet, plain curl, self-test.
|
||||
- Build once against edge; one artifact per package per architecture,
|
||||
published into every channel it belongs to (fast ring: all three at
|
||||
once). arch=any builds once for every architecture database.
|
||||
- Publish is incremental and immutable: pull the channel db, refuse
|
||||
different bytes under an existing name, accept identical bytes, upload
|
||||
packages then signatures then the db.
|
||||
- aarch64 under QEMU with credential-preserving binfmt. PR builds now run
|
||||
aarch64 natively on `ubuntu-24.04-arm` (QEMU was up to ~15x slower). When a
|
||||
merged tree has no artifact, publish.yml rebuilds it in its own job, aarch64
|
||||
there and x86_64 on a droplet, outside the publish lock.
|
||||
- Publish itself builds nothing: it signs and uploads on `ubuntu-latest` in the
|
||||
tested builder image pulled from GHCR, and only that job holds the `publish`
|
||||
concurrency group, so a merge waits for seconds of signing, not for builds.
|
||||
- Vouch gate: collaborators, `.github/VOUCHED.td`, or the `build-approved`
|
||||
label; denounced authors cannot be overridden by the label.
|
||||
- Tests run on PRs only; `result`, `self-tests`, `build-isolation` are the
|
||||
required checks with strict up-to-date branches.
|
||||
|
||||
## Cleanup
|
||||
|
||||
doctl compute droplet list --tag-name omarchy-builder
|
||||
doctl compute droplet delete -f <id>
|
||||
@@ -0,0 +1,45 @@
|
||||
#cloud-config
|
||||
# The always-on controller droplet (smallest size is fine). Clones the repo
|
||||
# for ci/controller.sh, installs the unit and timer, and starts polling.
|
||||
#
|
||||
# Substitute before use:
|
||||
# __REPO_URL__ https://github.com/omacom/omarchy-pkgs.git
|
||||
# __BRANCH__ branch carrying ci/ (master once merged)
|
||||
# __ENV_B64__ base64 of a filled-in controller.env.example
|
||||
# __SSH_KEYS_JSON__ JSON array of public keys authorized for root
|
||||
package_update: true
|
||||
packages: [curl, jq, git]
|
||||
|
||||
# Root stays reachable by key so the journal can be read. Two things stand
|
||||
# in the way on DO images: disable_root rewrites root's keys into a stub, and
|
||||
# with no account ssh key attached DO expires root's password, which makes
|
||||
# sshd refuse every non-interactive session with "password change required".
|
||||
disable_root: false
|
||||
chpasswd:
|
||||
expire: false
|
||||
ssh_authorized_keys: __SSH_KEYS_JSON__
|
||||
|
||||
users:
|
||||
- name: controller
|
||||
shell: /bin/bash
|
||||
|
||||
write_files:
|
||||
# defer: write after the users module has created the controller group,
|
||||
# otherwise chown to root:controller fails and the unit cannot read this.
|
||||
- path: /etc/omarchy-controller.env
|
||||
permissions: "0640"
|
||||
owner: root:controller
|
||||
encoding: b64
|
||||
defer: true
|
||||
content: __ENV_B64__
|
||||
|
||||
runcmd:
|
||||
- chage -d "$(date +%F)" -M -1 root
|
||||
- chown root:controller /etc/omarchy-controller.env && chmod 0640 /etc/omarchy-controller.env
|
||||
- git clone --depth 1 --branch __BRANCH__ __REPO_URL__ /opt/omarchy-pkgs
|
||||
- mkdir -p /run/omarchy-controller && chown controller:controller /run/omarchy-controller
|
||||
- echo "d /run/omarchy-controller 0755 controller controller -" > /etc/tmpfiles.d/omarchy-controller.conf
|
||||
# runcmd is executed by /bin/sh: no brace expansion.
|
||||
- cp /opt/omarchy-pkgs/ci/controller-box/omarchy-controller.service /opt/omarchy-pkgs/ci/controller-box/omarchy-controller.timer /etc/systemd/system/
|
||||
- systemctl daemon-reload
|
||||
- systemctl enable --now omarchy-controller.timer
|
||||
@@ -0,0 +1,19 @@
|
||||
# /etc/omarchy-controller.env — mode 0600, owned by root, read by systemd.
|
||||
DIGITALOCEAN_TOKEN=dop_v1_...
|
||||
# Fine-grained PAT scoped to the repo: Actions: read, Administration: read+write
|
||||
GITHUB_TOKEN=github_pat_...
|
||||
REPO=omacom/omarchy-pkgs
|
||||
LABEL=omarchy-builder
|
||||
TAG=omarchy-builder
|
||||
# Builder sizes, tried in order in any region that has them in stock.
|
||||
SIZES="g5-32vcpu-64gb-50gb g5-32vcpu-128gb-50gb"
|
||||
# Optional: regions to try first, e.g. "ric1". Empty means any.
|
||||
REGIONS=
|
||||
MAX_DROPLETS=6
|
||||
MAX_AGE_MINUTES=200
|
||||
# Delete a droplet DigitalOcean still reports as provisioning after this long.
|
||||
MAX_BOOT_MINUTES=10
|
||||
LOCK=/run/omarchy-controller/lock
|
||||
# Operator public keys for root on every builder droplet (JSON array).
|
||||
# create.sh fills this from the operators' GitHub keys.
|
||||
SSH_KEYS_JSON=[]
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# Create the controller droplet with plain curl. Run from a laptop, once.
|
||||
#
|
||||
# DIGITALOCEAN_TOKEN=... GITHUB_TOKEN=... ci/controller-box/create.sh [branch]
|
||||
#
|
||||
# The DO token given here is baked into the box's env file, so it must be the
|
||||
# token for the account that should pay for builder droplets.
|
||||
set -euo pipefail
|
||||
here=$(dirname "$0")
|
||||
: "${DIGITALOCEAN_TOKEN:?}" "${GITHUB_TOKEN:?}"
|
||||
REPO=${REPO:-omacom/omarchy-pkgs}
|
||||
BRANCH=${1:-master}
|
||||
REGION=${REGION:-ric1}
|
||||
NAME=${NAME:-omarchy-controller}
|
||||
# Optional DO ssh key ids as a JSON array, e.g. SSH_KEYS='[123]', for reading
|
||||
# the journal while bringing the box up. Not needed once it works.
|
||||
SSH_KEYS=${SSH_KEYS:-[]}
|
||||
# Public keys authorized for root: the operators' GitHub keys, fetched at
|
||||
# creation so the box never depends on an ssh_key API scope. Override with
|
||||
# ADMIN_GITHUB_USERS.
|
||||
ADMIN_GITHUB_USERS=${ADMIN_GITHUB_USERS:-ryanrhughes dhh}
|
||||
ssh_keys_json=$(for u in $ADMIN_GITHUB_USERS; do curl -fsS "https://github.com/$u.keys"; done | jq -R . | jq -sc .)
|
||||
[[ $(jq length <<<"$ssh_keys_json") -gt 0 ]] || { echo "no ssh keys fetched for $ADMIN_GITHUB_USERS" >&2; exit 1; }
|
||||
|
||||
env_file=$(sed -e "s|^DIGITALOCEAN_TOKEN=.*|DIGITALOCEAN_TOKEN=$DIGITALOCEAN_TOKEN|" \
|
||||
-e "s|^GITHUB_TOKEN=.*|GITHUB_TOKEN=$GITHUB_TOKEN|" \
|
||||
-e "s|^REPO=.*|REPO=$REPO|" \
|
||||
-e "s|^SSH_KEYS_JSON=.*|SSH_KEYS_JSON=$ssh_keys_json|" "$here/controller.env.example")
|
||||
userdata=$(sed -e "s|__REPO_URL__|https://github.com/$REPO.git|" -e "s|__BRANCH__|$BRANCH|" \
|
||||
-e "s|__ENV_B64__|$(printf '%s\n' "$env_file" | base64 -w0)|" \
|
||||
-e "s|__SSH_KEYS_JSON__|$ssh_keys_json|" "$here/cloud-init.yaml")
|
||||
body=$(jq -n --arg name "$NAME" --arg region "$REGION" --arg ud "$userdata" --argjson keys "$SSH_KEYS" \
|
||||
'{name:$name, region:$region, size:"s-1vcpu-1gb", image:"ubuntu-24-04-x64", tags:["omarchy-controller"], user_data:$ud, ssh_keys:$keys}')
|
||||
|
||||
# Refuse to create a second one.
|
||||
existing=$(curl -fsS -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
|
||||
"https://api.digitalocean.com/v2/droplets?tag_name=omarchy-controller" | jq '.droplets | length')
|
||||
if (( existing > 0 )); then echo "a controller droplet already exists" >&2; exit 1; fi
|
||||
|
||||
curl -fsS -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" -H "Content-Type: application/json" \
|
||||
-X POST -d "$body" https://api.digitalocean.com/v2/droplets | jq -r '"created \(.droplet.name) id=\(.droplet.id)"'
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Provision ephemeral omarchy-builder runner droplets for queued jobs
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=controller
|
||||
EnvironmentFile=/etc/omarchy-controller.env
|
||||
# Run the branch's current controller, not the one cloned when the box was
|
||||
# built. As root (the checkout's owner); a failed pull keeps the last one.
|
||||
ExecStartPre=-+/usr/bin/git -C /opt/omarchy-pkgs pull --ff-only --quiet
|
||||
ExecStart=/opt/omarchy-pkgs/ci/controller.sh
|
||||
# The reaper's safety net is time, not state; a hung tick must not hold the lock.
|
||||
TimeoutStartSec=240
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Run the omarchy-builder controller every minute
|
||||
|
||||
[Timer]
|
||||
OnBootSec=1min
|
||||
OnUnitActiveSec=1min
|
||||
AccuracySec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Executable
+195
@@ -0,0 +1,195 @@
|
||||
#!/bin/bash
|
||||
# Droplet-per-job controller for the omarchy-builder runner pool.
|
||||
#
|
||||
# Run from a systemd timer every minute on a small always-on droplet. No
|
||||
# inbound endpoint: it polls GitHub for queued jobs wanting our label, creates
|
||||
# one ephemeral droplet per job (up to MAX_DROPLETS), and deletes droplets
|
||||
# that have powered off, exceeded MAX_AGE_MINUTES, or are still provisioning
|
||||
# after MAX_BOOT_MINUTES. The reaper does not
|
||||
# trust its own bookkeeping: it lists by tag and acts on what DigitalOcean
|
||||
# reports.
|
||||
#
|
||||
# Talks to both APIs with curl. No doctl: its saved contexts silently choose
|
||||
# an account; a token in the environment cannot. Needs curl and jq.
|
||||
#
|
||||
# Environment:
|
||||
# DIGITALOCEAN_TOKEN DO API token for the account that pays for droplets
|
||||
# GITHUB_TOKEN fine-grained PAT: Actions read, Administration write
|
||||
# REPO owner/name
|
||||
set -euo pipefail
|
||||
|
||||
REPO=${REPO:?owner/name}
|
||||
: "${DIGITALOCEAN_TOKEN:?}" "${GITHUB_TOKEN:?}"
|
||||
LABEL=${LABEL:-omarchy-builder}
|
||||
TAG=${TAG:-omarchy-builder}
|
||||
# Sizes to try, in order, in any region DigitalOcean lists them in stock. A
|
||||
# size can sell out in a region for hours; the create is then refused with
|
||||
# 422 and the next region, then the next size, is tried. REGIONS only orders
|
||||
# the regions tried first. SIZE and REGION, if set, are one-item lists.
|
||||
SIZES=${SIZES:-${SIZE:-g5-32vcpu-64gb-50gb g5-32vcpu-128gb-50gb}}
|
||||
REGIONS=${REGIONS:-${REGION:-}}
|
||||
IMAGE=${IMAGE:-ubuntu-24-04-x64}
|
||||
MAX_DROPLETS=${MAX_DROPLETS:-4}
|
||||
MAX_AGE_MINUTES=${MAX_AGE_MINUTES:-200}
|
||||
# A droplet DigitalOcean still reports as "new" this long after creation is
|
||||
# stuck provisioning. Left alone it counts as a runner booting, and holds a
|
||||
# queued job until MAX_AGE_MINUTES.
|
||||
MAX_BOOT_MINUTES=${MAX_BOOT_MINUTES:-10}
|
||||
RUNNER_VERSION=${RUNNER_VERSION:-2.337.0}
|
||||
CLOUD_INIT=${CLOUD_INIT:-$(dirname "$0")/runner-cloud-init.yaml}
|
||||
# Operator public keys authorized on every builder (JSON array of strings).
|
||||
# The box's env file carries them; empty means no root login.
|
||||
SSH_KEYS_JSON=${SSH_KEYS_JSON:-[]}
|
||||
LOCK=${LOCK:-/tmp/omarchy-controller.lock}
|
||||
|
||||
log() { echo "$(date '+%F %T') $*"; }
|
||||
|
||||
# The only two places the outside world is touched. The self-test overrides
|
||||
# both, so every decision below is exercised against canned responses.
|
||||
do_api() { # do_api <path> [curl args...]
|
||||
local path=$1; shift
|
||||
# --fail-with-body: a refused create still prints why.
|
||||
curl -sS --fail-with-body -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
|
||||
-H "Content-Type: application/json" "https://api.digitalocean.com/v2/$path" "$@"
|
||||
}
|
||||
gh_api() { # gh_api <path> [curl args...]
|
||||
local path=$1; shift
|
||||
curl -fsS -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" "https://api.github.com/$path" "$@"
|
||||
}
|
||||
|
||||
# --- reap ------------------------------------------------------------------
|
||||
reap() {
|
||||
local now id status created age
|
||||
now=$(date +%s)
|
||||
while read -r id status created; do
|
||||
[[ -n "$id" ]] || continue
|
||||
age=$(( (now - $(date -d "$created" +%s)) / 60 ))
|
||||
if [[ $status == off ]] || (( age > MAX_AGE_MINUTES )) ||
|
||||
{ [[ $status == new ]] && (( age > MAX_BOOT_MINUTES )); }; then
|
||||
log "deleting droplet $id (status=$status age=${age}m)"
|
||||
do_api "droplets/$id" -X DELETE
|
||||
fi
|
||||
done < <(do_api "droplets?tag_name=$TAG&per_page=200" |
|
||||
jq -r '.droplets[] | "\(.id) \(.status) \(.created_at)"')
|
||||
}
|
||||
|
||||
# --- demand ----------------------------------------------------------------
|
||||
# Emit every item, including later pages of large build matrices. Keep API
|
||||
# failures fatal so a failed query cannot look like an empty queue.
|
||||
gh_items() {
|
||||
local path=$1 key=$2 page=1 response count separator="?"
|
||||
[[ $path != *"?"* ]] || separator="&"
|
||||
while :; do
|
||||
response=$(gh_api "${path}${separator}per_page=100&page=$page") || return 1
|
||||
count=$(jq -er --arg key "$key" '.[$key] | arrays | length' <<< "$response") || return 1
|
||||
jq -c --arg key "$key" '.[$key][]' <<< "$response" || return 1
|
||||
(( count == 100 )) || break
|
||||
((page += 1))
|
||||
done
|
||||
}
|
||||
|
||||
queued_jobs() {
|
||||
local status runs run
|
||||
# A workflow can be in progress while most of its matrix is still queued.
|
||||
runs=$(
|
||||
for status in queued in_progress; do
|
||||
gh_items "repos/$REPO/actions/runs?status=$status" workflow_runs || exit 1
|
||||
done
|
||||
) || return 1
|
||||
jq -r '.id' <<< "$runs" | sort -u |
|
||||
while read -r run; do
|
||||
gh_items "repos/$REPO/actions/runs/$run/jobs" jobs |
|
||||
jq -r --arg l "$LABEL" 'select(.status=="queued") | select(.labels | index($l)) | .id' || return 1
|
||||
done | sort -u | wc -l
|
||||
}
|
||||
|
||||
live_droplets() {
|
||||
# Not the ones reap() just deleted: DigitalOcean can list them for a while.
|
||||
do_api "droplets?tag_name=$TAG&per_page=200" | jq --argjson boot "$MAX_BOOT_MINUTES" '
|
||||
[.droplets[] | select(.status != "off")
|
||||
| select(.status != "new" or (now - (.created_at | fromdateiso8601)) / 60 <= $boot)] | length'
|
||||
}
|
||||
|
||||
busy_runners() {
|
||||
gh_api "repos/$REPO/actions/runners?per_page=100" \
|
||||
| jq --arg l "$LABEL" '[.runners[] | select(.busy) | select(any(.labels[]; .name == $l))] | length'
|
||||
}
|
||||
|
||||
# --- capacity --------------------------------------------------------------
|
||||
# "size region" lines to try, best first: SIZES order, then REGIONS order,
|
||||
# then every other region where DigitalOcean lists the size in stock.
|
||||
candidates() {
|
||||
local page=1 response count catalog=""
|
||||
while :; do
|
||||
response=$(do_api "sizes?per_page=200&page=$page") || return 1
|
||||
count=$(jq -er '.sizes | arrays | length' <<< "$response") || return 1
|
||||
catalog+=$(jq -c '.sizes[]' <<< "$response")$'\n'
|
||||
(( count == 200 )) || break
|
||||
((page += 1))
|
||||
done
|
||||
jq -rs --arg sizes "$SIZES" --arg regions "$REGIONS" '
|
||||
($regions | split(" ") | map(select(length > 0))) as $pref
|
||||
| INDEX(.slug) as $by
|
||||
| $sizes | split(" ") | map(select(length > 0)) | .[]
|
||||
| . as $size | $by[$size] // {} | select(.available == true)
|
||||
| .regions as $in
|
||||
| (($pref | map(select(. as $r | $in | index($r)))) + ($in - $pref))[]
|
||||
| "\($size) \(.)"' <<< "$catalog"
|
||||
}
|
||||
|
||||
# --- create ----------------------------------------------------------------
|
||||
# Built once per tick by the first create; a refused pair is dropped from it.
|
||||
CANDIDATES=""
|
||||
create_droplet() {
|
||||
local token userdata name size region body response
|
||||
[[ -n $CANDIDATES ]] || CANDIDATES=$(candidates) || return 1
|
||||
token=$(gh_api "repos/$REPO/actions/runners/registration-token" -X POST | jq -r .token)
|
||||
userdata=$(sed -e "s|__REPO__|$REPO|g" -e "s|__RUNNER_TOKEN__|$token|g" \
|
||||
-e "s|__RUNNER_LABELS__|$LABEL|g" -e "s|__RUNNER_VERSION__|$RUNNER_VERSION|g" \
|
||||
-e "s|__SSH_KEYS_JSON__|$SSH_KEYS_JSON|" "$CLOUD_INIT")
|
||||
name="$TAG-$(date +%s)-$RANDOM"
|
||||
while read -r size region; do
|
||||
[[ -n $size ]] || continue
|
||||
body=$(jq -n --arg name "$name" --arg region "$region" --arg size "$size" --arg image "$IMAGE" \
|
||||
--arg tag "$TAG" --arg ud "$userdata" \
|
||||
'{name:$name, region:$region, size:$size, image:$image, tags:[$tag], user_data:$ud, monitoring:false}')
|
||||
log "creating $name ($size in $region)"
|
||||
if response=$(do_api droplets -X POST -d "$body"); then
|
||||
jq -r '"created droplet \(.droplet.id)"' <<< "$response"
|
||||
return 0
|
||||
fi
|
||||
log "$size in $region refused: $(jq -r .message <<< "$response" 2>/dev/null || echo "$response")"
|
||||
CANDIDATES=$(grep -Fvx "$size $region" <<< "$CANDIDATES" || true)
|
||||
done <<< "$CANDIDATES"
|
||||
# Every size refused everywhere: the rest of this tick's creates would be too.
|
||||
log "no size in '$SIZES' can be created in any region"
|
||||
return 1
|
||||
}
|
||||
|
||||
controller_tick() {
|
||||
CANDIDATES=""
|
||||
reap
|
||||
local queued live busy available need room
|
||||
queued=$(queued_jobs)
|
||||
live=$(live_droplets)
|
||||
busy=$(busy_runners)
|
||||
# A live droplet whose runner is busy is spoken for. Only droplets still
|
||||
# booting or listening can absorb a queued job.
|
||||
available=$(( live - busy )); (( available < 0 )) && available=0
|
||||
need=$(( queued - available ))
|
||||
(( need > 0 )) || return 0
|
||||
room=$(( MAX_DROPLETS - live ))
|
||||
(( need > room )) && need=$room
|
||||
if (( need <= 0 )); then
|
||||
log "at cap ($live/$MAX_DROPLETS, $busy busy) with $queued queued"
|
||||
return 0
|
||||
fi
|
||||
local i
|
||||
for (( i = 0; i < need; i++ )); do create_droplet; done
|
||||
}
|
||||
|
||||
if [[ "${CONTROLLER_LIBRARY_ONLY:-}" != 1 ]]; then
|
||||
exec 9>"$LOCK"; flock -n 9 || exit 0
|
||||
controller_tick
|
||||
fi
|
||||
@@ -0,0 +1,83 @@
|
||||
#cloud-config
|
||||
# Ephemeral GitHub Actions runner for omarchy-pkgs package builds.
|
||||
#
|
||||
# Boots an Ubuntu droplet, installs docker and the runner, registers ONCE with
|
||||
# --ephemeral, runs exactly one job, then powers off. The controller (or the
|
||||
# reaper) deletes the powered-off droplet. Nothing here holds a long-lived
|
||||
# credential: the registration token is single-use and expires in an hour.
|
||||
#
|
||||
# Substitute before use:
|
||||
# __REPO__ owner/name
|
||||
# __RUNNER_TOKEN__ registration token (gh api -X POST repos/O/R/actions/runners/registration-token)
|
||||
# __RUNNER_LABELS__ e.g. omarchy-builder
|
||||
# __RUNNER_VERSION__ e.g. 2.329.0
|
||||
|
||||
# Operators can reach a builder by key while it lives; it powers off after
|
||||
# one job anyway. Keys are substituted by the controller (__SSH_KEYS_JSON__).
|
||||
disable_root: false
|
||||
chpasswd:
|
||||
expire: false
|
||||
ssh_authorized_keys: __SSH_KEYS_JSON__
|
||||
|
||||
package_update: true
|
||||
packages:
|
||||
- docker.io
|
||||
- docker-buildx
|
||||
- unzip
|
||||
- git
|
||||
- curl
|
||||
- jq
|
||||
- rsync
|
||||
|
||||
users:
|
||||
- name: runner
|
||||
groups: [docker]
|
||||
shell: /bin/bash
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
|
||||
write_files:
|
||||
# defer: write after users/groups exist, so /home/runner is created by
|
||||
# useradd (owned by runner) rather than by this module as root.
|
||||
- path: /home/runner/start.sh
|
||||
permissions: "0755"
|
||||
owner: runner:runner
|
||||
defer: true
|
||||
content: |
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
# Power off after the one job, and also when the download or the
|
||||
# registration fails: the controller deletes powered-off droplets, but
|
||||
# counts a running one as a runner still booting until MAX_AGE_MINUTES.
|
||||
trap 'sudo poweroff' EXIT
|
||||
cd /home/runner
|
||||
mkdir -p actions-runner && cd actions-runner
|
||||
arch=$(uname -m); [[ $arch == x86_64 ]] && arch=x64
|
||||
curl -fsSL -o runner.tgz \
|
||||
"https://github.com/actions/runner/releases/download/v__RUNNER_VERSION__/actions-runner-linux-${arch}-__RUNNER_VERSION__.tar.gz"
|
||||
tar xzf runner.tgz && rm runner.tgz
|
||||
./config.sh --unattended --ephemeral \
|
||||
--url "https://github.com/__REPO__" \
|
||||
--token "__RUNNER_TOKEN__" \
|
||||
--name "do-$(hostname)" \
|
||||
--labels "__RUNNER_LABELS__" \
|
||||
--replace
|
||||
./run.sh
|
||||
|
||||
runcmd:
|
||||
# With no account ssh key attached, DO expires root's password, and sshd
|
||||
# then refuses every non-interactive session. Clear it first so operators
|
||||
# can read the logs of a builder that never registers.
|
||||
- chage -d "$(date +%F)" -M -1 root
|
||||
- systemctl enable --now docker
|
||||
# aarch64 builds run under user-mode emulation (DO has no arm droplets).
|
||||
# Register QEMU with the F and C flags via tonistiigi/binfmt, exactly as
|
||||
# helpers/docker-helpers.sh setup_qemu does. Ubuntu's qemu-user-static
|
||||
# registers without C, so sudo inside the emulated container fails with
|
||||
# "effective uid is not 0"; multiarch/qemu-user-static is abandoned at QEMU
|
||||
# 7.2, under which qmake's compiler probe returns nothing on current gcc
|
||||
# ("failed to parse default include paths", PR #517). Pin the emulator
|
||||
# version: the tag is the only thing that decides what every aarch64 build
|
||||
# runs under. Best-effort: an x86-only job never needs it.
|
||||
- docker run --rm --privileged docker.io/tonistiigi/binfmt:qemu-v10.2.3-68 --uninstall qemu-aarch64 --install arm64 || true
|
||||
- chown -R runner:runner /home/runner
|
||||
- sudo -u runner /home/runner/start.sh > /home/runner/runner.log 2>&1
|
||||
@@ -0,0 +1,222 @@
|
||||
# Direct upstream watches
|
||||
|
||||
Omarchy owns the recipes in `pkgbuilds/`. `bin/sync-upstream` discovers new
|
||||
releases directly from project/vendor feeds and updates versions, declared
|
||||
release variables, and the source checksums already used by the recipe. It never
|
||||
imports upstream PKGBUILDs or runs downloaded build scripts. Architecture support,
|
||||
root install hooks, dependencies, and build functions remain ours to maintain.
|
||||
|
||||
`upstream.watch` complements the existing declarative providers and custom hooks:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "abenz1267/walker",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Watch fields
|
||||
|
||||
Choose exactly one provider: `github` (owner/repository), `git_tags` (repository
|
||||
HTTPS URL), `git_branch` (repository URL plus explicit `branch`), `npm` or `pypi`
|
||||
(package name), `debian` (Packages index plus exact `package`), `json` (URL plus
|
||||
version `path`), `regex` (text URL), `redirect` (final HTTPS download URL), or
|
||||
`archive` (inspect archive metadata without extracting/executing code).
|
||||
|
||||
Tag, text, redirect and archive watches use an explicit `pattern` with a named
|
||||
`version` capture. Tag patterns match the entire tag. `version` optionally formats
|
||||
those captures into an Arch pkgver; e.g. Sublime uses `4.{version}`. JSON feeds can
|
||||
expose additional capture values through `fields`, a name-to-JSON-path map.
|
||||
|
||||
`variables` maps recipe scalars such as `_commit` or `_build` to capture templates.
|
||||
Only explicitly declared underscore-prefixed variables can change. GitHub
|
||||
`{commit}` resolves the selected tag, not a moving target_commitish branch.
|
||||
`submodules` can map a recipe variable to a gitlink in the selected GitHub tag;
|
||||
RustDesk uses this for hbb_common. Downloaded repository code is never evaluated.
|
||||
|
||||
For upstreams that rebuild a release, declare a numeric `revision` template and
|
||||
its `revision_variable`. With unchanged pkgver, only an increasing revision can
|
||||
advance that variable, and the downstream pkgrel increments instead of resetting.
|
||||
Cursor CLI uses `sequence` to preserve its date/counter/hash version convention
|
||||
when the vendor publishes a second hash on the same day. A new pkgver resets
|
||||
pkgrel to 1, but the complete epoch:pkgver-pkgrel must still increase.
|
||||
|
||||
GitHub releases exclude drafts and prereleases unless `allow_prerelease` is true.
|
||||
Existing `min_release_age` policies apply: a feed without a verifiable publication
|
||||
time cannot bypass a configured hold.
|
||||
|
||||
## Branch watches
|
||||
|
||||
A `git_branch` watch treats every commit on a branch as a release and writes an
|
||||
immutable pin (`"_commit": "{commit}"`) so the recipe never carries a moving
|
||||
`#branch=` source; `tests/pinned-sources.sh` enforces that. The clone is bare,
|
||||
blobless and single-branch, read only with git, and shared by every package
|
||||
that watches the same branch in one run, so two recipes pinned from it always
|
||||
see the same commit. Values available to `version`:
|
||||
|
||||
- `{date}` (default), `{count}` (commits on the branch), `{commit}`
|
||||
(`{commit:.7}` for the short form)
|
||||
- with `tag_pattern` (a regular expression with a named `version` group,
|
||||
matched against whole tags): `{tag}`, `{version}` from that tag, and
|
||||
`{distance}`, the number of commits past it. Only tags in the pinned
|
||||
commit's own history count, so a release cut on another branch is ignored.
|
||||
|
||||
`{version}.r{distance}.g{commit:.7}` gives `1.21.0.r15.gabc1234`, which pacman
|
||||
orders above the `1.21.0` release it follows and below `1.21.1`; `omasnap-git`
|
||||
uses it. The Omarchy dev pair uses `{version}.r{count}.g{commit:.7}` instead
|
||||
because its published history counted every commit and the number must never
|
||||
go down.
|
||||
|
||||
`min_release_age` holds a branch tip until its commit timestamp is old enough.
|
||||
A fresh tip leaves the existing pin alone; the watch never walks backward to
|
||||
an older commit. This uses Git's committer date, not the time a commit was
|
||||
pushed. `BYPASS_MIN_RELEASE_AGE=1` bypasses the hold.
|
||||
|
||||
Packages marked `"auto_merge": true` ride the unattended lane
|
||||
(`track-branches.yml`) instead of the reviewed sync PR: their bump PR is opened
|
||||
and auto-merged as soon as the build checks pass. `bin/sync-upstream --lane
|
||||
reviewed|auto-merge|all` selects a lane; the scheduled workflows each pass their
|
||||
own. Packages that pin the same branch move in lockstep: if one of them fails
|
||||
to update, the run restores the others and reports the group as failed. A
|
||||
targeted sync includes the other packages watching that branch, so requesting
|
||||
only `omarchy-dev` also updates `omarchy-settings-dev`.
|
||||
|
||||
### Enable unattended branch updates
|
||||
|
||||
The schedule already runs in GitHub Actions; no server cron job is needed.
|
||||
It uses a personal access token so its PRs trigger builds and its merges trigger
|
||||
publishing without manual approval. No GitHub App is required.
|
||||
|
||||
1. Use a fine-grained PAT with access to **omacom/omarchy-pkgs** and repository
|
||||
**Contents: Read and write** and **Pull requests: Read and write** permissions.
|
||||
Its owner must be trusted by the build workflow (for example, a collaborator).
|
||||
The existing controller PAT can be reused when it has these permissions.
|
||||
2. In the repository's
|
||||
[Actions secrets](https://github.com/omacom/omarchy-pkgs/settings/secrets/actions),
|
||||
save the PAT as `PKGS_BOT_TOKEN`. Update this secret when the token is rotated
|
||||
or expires. The built-in Actions `GITHUB_TOKEN` cannot run this unattended chain.
|
||||
3. Keep **Allow auto-merge** enabled and require `result`, `self-tests`, and
|
||||
`build-isolation` on `master`; the tracker does not request a protection bypass.
|
||||
4. After merging the tracker, run **Track upstream branches** once from Actions
|
||||
to verify that its PR builds, auto-merges, and starts **Publish merged packages**.
|
||||
Subsequent runs happen every two hours.
|
||||
|
||||
Checksums retain their algorithms (SHA256, SHA512, BLAKE2, etc.) and source order.
|
||||
Changed git sources are hashed with makepkg's git-archive convention. Unchanged
|
||||
sources retain their hashes; `mutable_sources` explicitly names entries such as
|
||||
`source:0` that must be fetched again for a new version despite a stable URL.
|
||||
Existing `SKIP` entries remain unchanged (including signed metadata verified by
|
||||
the recipe); new skips are never introduced. Changed URLs are still fetched.
|
||||
A matching GitHub release asset SHA256 digest avoids downloading large assets.
|
||||
Missing architecture artifacts or malformed metadata fail the package atomically.
|
||||
Every declared architecture must read back the same release and checksum values.
|
||||
|
||||
Archive watches use `member` to select a text member, or `filenames: true` to read
|
||||
versions from archive member names. Debian archives are read through their control
|
||||
metadata. `unescape_json` handles JSON strings embedded in a vendor's HTML page.
|
||||
|
||||
## Maintenance and validation
|
||||
|
||||
Running watches locally requires Python 3.11+, Bash, curl, git, jq, Arch's
|
||||
`vercmp`, and `bsdtar`. CI installs these in its Arch container.
|
||||
|
||||
- Edit packaging and architecture changes directly in PKGBUILD. The old AUR
|
||||
overlays have been folded into these recipes and removed.
|
||||
- Keep source-code patches and install hooks checked in as ordinary package files.
|
||||
- Bump pkgrel when changing a recipe at the same version. Removing a dotted AUR
|
||||
suffix must never lower the complete version.
|
||||
- Add a watch with each new package. `bin/add-package --source aur` is a one-time
|
||||
import; it records historical `origin` metadata and leaves an owned recipe.
|
||||
- `python helpers/upstream-watch.py check pkgbuilds/NAME` checks release discovery
|
||||
without rewriting the recipe. `bin/sync-upstream NAME` performs the update.
|
||||
- `python tests/upstream-watch.py` tests update atomicity, architecture coverage,
|
||||
version ordering, source hashes and hostile metadata using offline fixtures.
|
||||
|
||||
The scheduled workflow continues reviewing completed updates if another package
|
||||
fails. The failing recipe stays unchanged and the run still reports failure.
|
||||
|
||||
## Migrated package watches
|
||||
|
||||
68 active AUR packages now use direct watches. The nine previously disabled
|
||||
packages retain manual maintenance holds. Historical AUR provenance is recorded
|
||||
in `origin` and has no effect on release selection.
|
||||
|
||||
| Package | Provider | Upstream |
|
||||
|---|---|---|
|
||||
| `1password-beta` | debian | [https://downloads.1password.com/linux/debian/amd64/dists/beta/main/binary-amd64/Packages](https://downloads.1password.com/linux/debian/amd64/dists/beta/main/binary-amd64/Packages) |
|
||||
| `1password-cli` | json | [https://app-updates.agilebits.com/check/1/0/CLI2/en/0](https://app-updates.agilebits.com/check/1/0/CLI2/en/0) |
|
||||
| `aether` | github | [omacom/aether](https://github.com/omacom/aether) |
|
||||
| `basecamp-cli` | github | [basecamp/basecamp-cli](https://github.com/basecamp/basecamp-cli) |
|
||||
| `bun-bin` | github | [oven-sh/bun](https://github.com/oven-sh/bun) |
|
||||
| `claude-code` | regex | [https://downloads.claude.ai/claude-code-releases/latest](https://downloads.claude.ai/claude-code-releases/latest) |
|
||||
| `cliamp` | github | [bjarneo/cliamp](https://github.com/bjarneo/cliamp) |
|
||||
| `crush-bin` | github | [charmbracelet/crush](https://github.com/charmbracelet/crush) |
|
||||
| `cursor-bin` | json | [https://www.cursor.com/api/download?platform=linux-x64&releaseTrack=stable](https://www.cursor.com/api/download?platform=linux-x64&releaseTrack=stable) |
|
||||
| `cursor-cli` | regex | [https://cursor.com/install](https://cursor.com/install) |
|
||||
| `dbxcli-bin` | github | [dropbox/dbxcli](https://github.com/dropbox/dbxcli) |
|
||||
| `dropbox` | redirect | [https://www.dropbox.com/download?plat=lnx.x86_64](https://www.dropbox.com/download?plat=lnx.x86_64) |
|
||||
| `dropbox-cli` | regex | [https://linux.dropbox.com/packages/](https://linux.dropbox.com/packages/) |
|
||||
| `heroic-games-launcher-bin` | github | [Heroic-Games-Launcher/HeroicGamesLauncher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher) |
|
||||
| `hyprshade` | pypi | [hyprshade](https://pypi.org/project/hyprshade/) |
|
||||
| `lib32-nvidia-580xx-utils` | regex | [https://download.nvidia.com/XFree86/Linux-x86_64/](https://download.nvidia.com/XFree86/Linux-x86_64/) |
|
||||
| `limine-mkinitcpio-hook` | git_tags | [https://gitlab.com/Zesko/limine-entry-tool.git](https://gitlab.com/Zesko/limine-entry-tool.git) |
|
||||
| `limine-snapper-sync` | git_tags | [https://gitlab.com/Zesko/limine-snapper-sync.git](https://gitlab.com/Zesko/limine-snapper-sync.git) |
|
||||
| `lmstudio-bin` | regex | [https://lmstudio.ai/download](https://lmstudio.ai/download) |
|
||||
| `localsend` | github | [localsend/localsend](https://github.com/localsend/localsend) |
|
||||
| `localsend-bin` | github | [localsend/localsend](https://github.com/localsend/localsend) |
|
||||
| `macbook12-spi-driver-dkms` | git_branch | [https://github.com/marc-git/macbook12-spi-driver.git](https://github.com/marc-git/macbook12-spi-driver.git) |
|
||||
| `omarchy-dev`, `omarchy-settings-dev` | git_branch (auto-merge) | [https://github.com/basecamp/omarchy.git](https://github.com/basecamp/omarchy.git) `quattro` |
|
||||
| `omasnap-git` | git_branch (auto-merge) | [https://github.com/omacom/omasnap.git](https://github.com/omacom/omasnap.git) `main` |
|
||||
| `makima-bin` | github | [cyber-sushi/makima](https://github.com/cyber-sushi/makima) |
|
||||
| `minecraft-launcher` | archive | [https://launcher.mojang.com/download/Minecraft.deb](https://launcher.mojang.com/download/Minecraft.deb) |
|
||||
| `nautilus-dropbox` | github | [dropbox/nautilus-dropbox](https://github.com/dropbox/nautilus-dropbox) |
|
||||
| `nautilus-open-any-terminal` | git_tags | [https://github.com/Stunkymonkey/nautilus-open-any-terminal.git](https://github.com/Stunkymonkey/nautilus-open-any-terminal.git) |
|
||||
| `nordvpn-bin` | debian | [https://repo.nordvpn.com/deb/nordvpn/debian/dists/stable/main/binary-amd64/Packages](https://repo.nordvpn.com/deb/nordvpn/debian/dists/stable/main/binary-amd64/Packages) |
|
||||
| `nvidia-580xx-utils` | regex | [https://download.nvidia.com/XFree86/Linux-x86_64/](https://download.nvidia.com/XFree86/Linux-x86_64/) |
|
||||
| `omarchy-chromium-bin` | github | [omacom/omarchy-chromium](https://github.com/omacom/omarchy-chromium) |
|
||||
| `omarchy-emacs` | git_tags | [https://github.com/scottjones/omarchy-emacs.git](https://github.com/scottjones/omarchy-emacs.git) |
|
||||
| `omazed` | git_tags | [https://github.com/aps6/omazed.git](https://github.com/aps6/omazed.git) |
|
||||
| `once-bin` | github | [basecamp/once](https://github.com/basecamp/once) |
|
||||
| `openai-codex-bin` | github | [openai/codex](https://github.com/openai/codex) |
|
||||
| `python-mediapipe` | github | [google-ai-edge/mediapipe](https://github.com/google-ai-edge/mediapipe) |
|
||||
| `python-sounddevice` | pypi | [sounddevice](https://pypi.org/project/sounddevice/) |
|
||||
| `python-terminaltexteffects` | pypi | [terminaltexteffects](https://pypi.org/project/terminaltexteffects/) |
|
||||
| `rustdesk` | github | [rustdesk/rustdesk](https://github.com/rustdesk/rustdesk) |
|
||||
| `spotify` | debian | [https://repository.spotify.com/dists/testing/non-free/binary-amd64/Packages](https://repository.spotify.com/dists/testing/non-free/binary-amd64/Packages) |
|
||||
| `sublime-text-4` | json | [https://www.sublimetext.com/updates/4/stable_update_check](https://www.sublimetext.com/updates/4/stable_update_check) |
|
||||
| `sunshine` | github | [LizardByte/Sunshine](https://github.com/LizardByte/Sunshine) |
|
||||
| `ttf-ia-writer` | git_branch | [https://github.com/iaolo/iA-Fonts.git](https://github.com/iaolo/iA-Fonts.git) |
|
||||
| `tuxedo-drivers-nocompatcheck-dkms` | git_tags | [https://gitlab.com/kronerm/tuxedo-drivers-nocompatcheck.git](https://gitlab.com/kronerm/tuxedo-drivers-nocompatcheck.git) |
|
||||
| `typora` | debian | [https://downloads.typora.io/linux/Packages](https://downloads.typora.io/linux/Packages) |
|
||||
| `ufw-docker` | git_tags | [https://github.com/chaifeng/ufw-docker.git](https://github.com/chaifeng/ufw-docker.git) |
|
||||
| `vi` | regex | [https://sources.archlinux.org/other/vi/](https://sources.archlinux.org/other/vi/) |
|
||||
| `visual-studio-code-bin` | json | [https://update.code.visualstudio.com/api/update/linux-deb-x64/stable/latest](https://update.code.visualstudio.com/api/update/linux-deb-x64/stable/latest) |
|
||||
| `walker` | github | [abenz1267/walker](https://github.com/abenz1267/walker) |
|
||||
| `xpadneo-dkms` | github | [atar-axis/xpadneo](https://github.com/atar-axis/xpadneo) |
|
||||
| `yaru-icon-theme` | git_tags | [https://github.com/ubuntu/yaru.git](https://github.com/ubuntu/yaru.git) |
|
||||
| `yay` | github | [Jguer/yay](https://github.com/Jguer/yay) |
|
||||
| `yt6801-dkms` | archive | [https://www.motor-comm.com/Cn/Skippower/downloadFile.html?id=1817](https://www.motor-comm.com/Cn/Skippower/downloadFile.html?id=1817) |
|
||||
|
||||
## Existing manual holds
|
||||
|
||||
`libfprint-git`, `libretro-cap32-git`, `libretro-database-git`, `libretro-fbneo-git`, `libretro-uae-git`, `libretro-vice-git`, `quickshell-git`, `supergfxctl`.
|
||||
|
||||
These packages were already excluded from automatic AUR updates. The migration preserves that policy.
|
||||
|
||||
`m1n1-aurora` and `uboot-asahi` are deliberate holds: Apple Silicon boot code, pinned by hand like `linux-aurora`, and bumped only after a cold boot on the qualification Macs. `m1n1-aurora` pins an aurora-silicon/m1n1 commit plus a local patch. `uboot-asahi` follows asahi-alarm's recipe and patch set (asahi-alarm/PKGBUILDs), which a tag watch on AsahiLinux/u-boot cannot carry.
|
||||
|
||||
`cua-driver-bin` is a deliberate hold: Omarchy bumps it by hand, so a Cua release ships only when a maintainer has verified it. It keeps its `.omarchy/upstream.sh` hook and `min_release_age`, so lifting the hold means removing `"sync": false`. `cua-hyprland-plugin` declares no upstream source, so no automation updates it either.
|
||||
|
||||
## Package-specific boundaries
|
||||
|
||||
- NVIDIA watches remain on the 580 driver branch.
|
||||
- Hardware-specific packages keep their declared architectures; this migration does not invent ARM binaries for x86-only upstreams.
|
||||
- iA Duospace was deleted upstream. Its four legacy font files retain their original immutable pin while the other families track the current repository.
|
||||
- RustDesk reads hbb_common from the release gitlink; its existing build-time dependency/toolchain checks remain in force.
|
||||
- Spotify uses HTTPS and retains its signed Release/Packages verification.
|
||||
- Source and build compatibility still need review when upstream code changes. Direct watches remove AUR recipe churn, not the need to maintain packaging.
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# Package files cross from a PR build to publish.yml as one GitHub Actions
|
||||
# artifact. actions/upload-artifact rejects any path containing ':', and a
|
||||
# package with an epoch is named `name-1:ver-rel-arch.pkg.tar.zst` by
|
||||
# makepkg. So the files ride inside a tar with a plain name and keep their
|
||||
# own names untouched: pacman clients and bin/publish-artifact both rely on
|
||||
# the filename matching PKGINFO.
|
||||
#
|
||||
# Both functions run under the workflow's `bash -e`: nothing in them may
|
||||
# return non-zero except the final failure.
|
||||
|
||||
# package_files <dir>: the *.pkg.tar.zst directly in <dir>, one per line.
|
||||
# Signatures and the scratch database next to them are not packages.
|
||||
package_files() {
|
||||
local f
|
||||
for f in "$1"/*.pkg.tar.zst; do
|
||||
[[ -e "$f" ]] && printf '%s\n' "$f"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# pack_packages <dir> <tar>: every package in <dir> into <tar>.
|
||||
pack_packages() {
|
||||
local dir=$1 out=$2 files=()
|
||||
mapfile -t files < <(package_files "$dir")
|
||||
(( ${#files[@]} )) || { echo "pack_packages: no *.pkg.tar.zst in $dir" >&2; return 1; }
|
||||
tar -cf "$out" -C "$dir" -- "${files[@]##*/}"
|
||||
}
|
||||
|
||||
# unpack_packages <artifact dir> <dest>: the packages an unzipped artifact
|
||||
# carried, into <dest>. Packed artifacts hold packages.tar; artifacts from
|
||||
# builds before packing hold the bare files. The bare form can go once
|
||||
# those artifacts have expired (7-day retention).
|
||||
unpack_packages() {
|
||||
local src=$1 dest=$2 files=()
|
||||
mkdir -p "$dest"
|
||||
if [[ -f "$src/packages.tar" ]]; then
|
||||
tar -xf "$src/packages.tar" -C "$dest"
|
||||
return 0
|
||||
fi
|
||||
mapfile -t files < <(package_files "$src")
|
||||
(( ${#files[@]} )) || { echo "unpack_packages: nothing to unpack in $src" >&2; return 1; }
|
||||
cp -- "${files[@]}" "$dest/"
|
||||
}
|
||||
+127
-25
@@ -1,25 +1,113 @@
|
||||
# Docker helper functions for Omarchy package build system
|
||||
# Container engine helpers for Omarchy package build system
|
||||
|
||||
check_docker() {
|
||||
if ! command -v docker &>/dev/null; then
|
||||
print_error "Docker is not installed"
|
||||
container_engine_supported() {
|
||||
[[ "$CONTAINER_ENGINE" == "docker" || "$CONTAINER_ENGINE" == "podman" ]]
|
||||
}
|
||||
|
||||
if [[ -z "${CONTAINER_ENGINE:-}" ]]; then
|
||||
for engine in docker podman; do
|
||||
if command -v "$engine" >/dev/null 2>&1 && "$engine" info >/dev/null 2>&1; then
|
||||
CONTAINER_ENGINE="$engine"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
export CONTAINER_ENGINE
|
||||
|
||||
# Rootless Podman otherwise maps the image's builder uid 1000 to a subordinate
|
||||
# host uid. keep-id makes files written through bind mounts belong to the user
|
||||
# who invoked the build.
|
||||
CONTAINER_RUN_ARGS=()
|
||||
if [[ "$CONTAINER_ENGINE" == "podman" ]]; then
|
||||
CONTAINER_RUN_ARGS+=("--userns=keep-id:uid=1000,gid=1000")
|
||||
fi
|
||||
|
||||
check_engine() {
|
||||
if [[ -n "$CONTAINER_ENGINE" ]] && ! container_engine_supported; then
|
||||
print_error "Unsupported CONTAINER_ENGINE: $CONTAINER_ENGINE (use docker or podman)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info &>/dev/null; then
|
||||
print_error "Docker daemon is not running"
|
||||
print_warning "Start Docker with: sudo systemctl start docker"
|
||||
if [[ -z "$CONTAINER_ENGINE" ]]; then
|
||||
print_error "No working container engine found (tried Docker, then Podman)"
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
print_warning "Docker is installed but unavailable. Start it with: sudo systemctl start docker"
|
||||
elif command -v podman >/dev/null 2>&1; then
|
||||
print_warning "Podman is installed but 'podman info' failed"
|
||||
else
|
||||
print_warning "Install Docker or Podman"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v "$CONTAINER_ENGINE" >/dev/null 2>&1; then
|
||||
print_error "$CONTAINER_ENGINE is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
|
||||
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||
print_error "Docker daemon is not running or is not accessible"
|
||||
print_warning "Start Docker with: sudo systemctl start docker"
|
||||
else
|
||||
print_error "Podman is not available to the current user"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
docker_native_arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64) echo x86_64 ;;
|
||||
aarch64 | arm64) echo aarch64 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
setup_qemu() {
|
||||
# Setup QEMU for building ARM64 packages on x86_64 hosts
|
||||
if ! docker run --rm --privileged multiarch/qemu-user-static --reset -p yes --credential yes >/dev/null 2>&1; then
|
||||
print_error "Failed to setup QEMU for ARM64 emulation"
|
||||
local target_arch="${1:-aarch64}"
|
||||
|
||||
if [[ "$CONTAINER_ENGINE" == "podman" ]]; then
|
||||
local registration="/proc/sys/fs/binfmt_misc/qemu-$target_arch"
|
||||
local packaged_registration="/usr/lib/binfmt.d/qemu-$target_arch-static.conf"
|
||||
local flags=""
|
||||
|
||||
if [[ -r "$registration" ]]; then
|
||||
flags=$(sed -n 's/^flags: //p' "$registration")
|
||||
fi
|
||||
|
||||
if [[ "$flags" == *F* && "$flags" == *C* ]]; then
|
||||
print_success "QEMU $target_arch emulation is registered"
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_error "Rootless Podman requires QEMU binfmt registration with the F and C flags"
|
||||
print_info "F keeps the emulator available inside containers; C lets container sudo preserve credentials."
|
||||
print_info "Configure it once with:"
|
||||
echo " sudo pacman -S --needed qemu-user-static qemu-user-static-binfmt"
|
||||
echo " sudo mkdir -p /etc/binfmt.d"
|
||||
echo " sed 's/:FP$/:FPC/' $packaged_registration | sudo tee /etc/binfmt.d/qemu-$target_arch-static.conf >/dev/null"
|
||||
echo " sudo systemctl restart systemd-binfmt"
|
||||
exit 1
|
||||
fi
|
||||
print_success "QEMU ARM64 emulation enabled"
|
||||
|
||||
# Register emulators for builds whose target differs from the host, with
|
||||
# the F and C flags (tonistiigi/binfmt always sets both). The image tag pins
|
||||
# the QEMU version every emulated build runs under; multiarch/qemu-user-static
|
||||
# stopped at QEMU 7.2, which breaks qmake's compiler probe on current gcc.
|
||||
# Keep ci/runner-cloud-init.yaml on the same tag. Uninstall first: install
|
||||
# leaves an existing registration (an older emulator) in place and exits 0.
|
||||
local platform_arch
|
||||
case "$target_arch" in
|
||||
aarch64) platform_arch=arm64 ;;
|
||||
x86_64) platform_arch=amd64 ;;
|
||||
*) platform_arch="$target_arch" ;;
|
||||
esac
|
||||
if ! "$CONTAINER_ENGINE" run --rm --privileged docker.io/tonistiigi/binfmt:qemu-v10.2.3-68 --uninstall "qemu-$target_arch" --install "$platform_arch" >/dev/null 2>&1; then
|
||||
print_error "Failed to set up QEMU emulation"
|
||||
exit 1
|
||||
fi
|
||||
print_success "QEMU emulation enabled"
|
||||
}
|
||||
|
||||
build_docker_image() {
|
||||
@@ -39,31 +127,45 @@ build_docker_image() {
|
||||
;;
|
||||
esac
|
||||
|
||||
print_info "Building Docker image for $arch ($platform) using $mirror mirror..."
|
||||
|
||||
docker buildx build \
|
||||
--platform "$platform" \
|
||||
--build-arg MIRROR="$mirror" \
|
||||
--load \
|
||||
-t "$image_tag" \
|
||||
-f "$build_dir/Dockerfile" \
|
||||
"$build_dir"
|
||||
print_info "Building container image for $arch ($platform) using $mirror mirror..."
|
||||
|
||||
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||
"$CONTAINER_ENGINE" buildx build \
|
||||
--platform "$platform" \
|
||||
--build-arg MIRROR="$mirror" \
|
||||
--load \
|
||||
-t "$image_tag" \
|
||||
-f "$build_dir/Dockerfile" \
|
||||
"$build_dir"
|
||||
else
|
||||
"$CONTAINER_ENGINE" build \
|
||||
--platform "$platform" \
|
||||
--build-arg MIRROR="$mirror" \
|
||||
-t "$image_tag" \
|
||||
-f "$build_dir/Dockerfile" \
|
||||
"$build_dir"
|
||||
fi
|
||||
}
|
||||
|
||||
get_platform_arg() {
|
||||
local arch="$1"
|
||||
case "$arch" in
|
||||
x86_64) echo "--platform linux/amd64" ;;
|
||||
aarch64) echo "--platform linux/arm64" ;;
|
||||
*) echo "" ;;
|
||||
x86_64) echo "--platform=linux/amd64" ;;
|
||||
aarch64) echo "--platform=linux/arm64" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
make_dir_writable() {
|
||||
local dir="$1"
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
if (( EUID == 0 )); then
|
||||
chmod -R 777 "$dir"
|
||||
else
|
||||
sudo chown -R $(id -u):$(id -g) "$dir" 2>/dev/null || chmod -R 777 "$dir"
|
||||
# chown can succeed on part of the tree and fail on the rest (files a
|
||||
# previous container left behind as another uid); the old `|| chmod`
|
||||
# fallback only ran when chown failed outright, leaving those files
|
||||
# unwritable. Always follow with chmod so the whole tree is usable.
|
||||
sudo chown -R "$(id -u):$(id -g)" "$dir" 2>/dev/null || true
|
||||
chmod -R 777 "$dir"
|
||||
fi
|
||||
}
|
||||
+180
-20
@@ -3,21 +3,24 @@
|
||||
# Expects package directories in $PKGBUILDS_DIR, each with:
|
||||
# .omarchy/package.json
|
||||
#
|
||||
# Minimal schema:
|
||||
# { "source": "aur" }
|
||||
# { "source": "aur", "sync": false }
|
||||
# { "source": "aur", "aur": "different-aur-name" }
|
||||
# { "source": "aur", "release_ring": "fast" }
|
||||
# { "source": "aur", "skip_build": true }
|
||||
# { "source": "aur", "pkgrel": { "suffix": 1, "offset": 1 } }
|
||||
# { "source": "aur", "rebuild_on": ["qt6-base"] }
|
||||
# Minimal schema (legacy source:aur remains readable for initial imports):
|
||||
# { "source": "local" }
|
||||
# { "source": "local", "sync": false }
|
||||
# { "source": "local", "release_ring": "fast" }
|
||||
# { "source": "local", "skip_build": true }
|
||||
# { "source": "local", "rebuild_on": ["qt6-base"] }
|
||||
# { "source": "local", "upstream": { "watch": { "github": "owner/repo", "pattern": "v(?P<version>[0-9.]+)" } } }
|
||||
# { "source": "local", "channels": ["edge"] }
|
||||
# { "source": "local", "channels": ["edge", "rc", "stable"] }
|
||||
# { "source": "local", "min_release_age": "24h" }
|
||||
# { "source": "local", "upstream": { "github": "owner/repo", "checksums": "SHASUMS256.txt", "assets": { "x86_64": "name-{tag}-x64.tar.xz" } } }
|
||||
# { "source": "local", "auto_merge": true, "upstream": { "watch": { "git_branch": "...", "branch": "main" } } }
|
||||
# { "source": "local", "upstream": { "github": "owner/repo", "checksums": "SHASUMS256.txt", "assets": { "x86_64": ["name-{tag}-x64.tar.xz"] } } }
|
||||
# { "source": "local", "upstream": { "github": "owner/repo", "digests": true, "assets": { "x86_64": "name-{tag}-x64.tar.xz" } } }
|
||||
# { "source": "local", "upstream": { "git_tags": "https://example/repo.git", "tag_pattern": "v{pkgver}", "sources": { "any": ["https://example/archive/{tag}.tar.gz"] } } }
|
||||
# { "source": "local", "upstream": { "npm": "@scope/package", "sources": { "any": ["{npm_tarball}"] } } }
|
||||
# { "source": "local", "upstream": { "debian": "https://example/debian/dists/stable/main/binary-amd64/Packages", "package": "example", "sources": { "any": ["https://example/releases/{pkgver}.tar.gz"] } } }
|
||||
#
|
||||
# bin/sync-aur also writes upstream_commit for AUR-backed packages, and
|
||||
# bin/import-aur records historical origin.aur and origin.commit;
|
||||
# bin/sync-rebuilds writes rebuilt_against for packages declaring rebuild_on.
|
||||
|
||||
if [[ -z "${PKGBUILDS_DIR:-}" ]]; then
|
||||
@@ -140,6 +143,67 @@ package_has_pkgbuild() {
|
||||
[[ -f "$pkgdir/PKGBUILD" ]]
|
||||
}
|
||||
|
||||
# Read one variable from a PKGBUILD the way makepkg would see it.
|
||||
#
|
||||
# makepkg always exports CARCH, so PKGBUILDs may branch on it at file scope
|
||||
# (per-architecture sources, tarball suffixes, even `return` for an
|
||||
# unsupported architecture). Sourcing without CARCH takes the wrong branch or
|
||||
# aborts partway, which leaves pkgver and pkgrel empty — and an empty version
|
||||
# never equals the published one, so the package is queued for a rebuild that
|
||||
# promotion then refuses. Every read of a PKGBUILD goes through here.
|
||||
#
|
||||
# Prints the value; exit status is that of `source PKGBUILD` itself, so a
|
||||
# caller can tell "variable empty" from "PKGBUILD could not be read".
|
||||
package_pkgbuild_var() {
|
||||
local pkgdir="$1"
|
||||
local var="$2"
|
||||
local arch="${3:-${ARCH:-x86_64}}"
|
||||
|
||||
(cd "$pkgdir" && env -u OMARCHY_SRC CARCH="$arch" bash -c '
|
||||
source PKGBUILD >/dev/null 2>&1
|
||||
rc=$?
|
||||
printf "%s\n" "${!1:-}"
|
||||
exit "$rc"
|
||||
' _ "$var")
|
||||
}
|
||||
|
||||
# The architectures declared by a PKGBUILD. Set CARCH while reading it so a
|
||||
# conditional arch=() assignment is evaluated for the architecture we are
|
||||
# actually checking, even when the repository host is a different one.
|
||||
package_arches() {
|
||||
local pkgdir="$1"
|
||||
local arch="${2:-${ARCH:-x86_64}}"
|
||||
|
||||
(cd "$pkgdir" && env -u OMARCHY_SRC CARCH="$arch" bash -c '
|
||||
source PKGBUILD >/dev/null 2>&1
|
||||
printf "%s\n" "${arch[*]}"
|
||||
')
|
||||
}
|
||||
|
||||
package_supports_arch() {
|
||||
local pkgdir="$1"
|
||||
local target="${2:-${ARCH:-x86_64}}"
|
||||
local arches
|
||||
|
||||
arches=$(package_arches "$pkgdir" "$target") || return 1
|
||||
case " $arches " in
|
||||
*" any "* | *" $target "*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# The channel DB indexes only its newest version, but older published archives
|
||||
# remain immutable. Both the scheduler and build planner must skip an existing
|
||||
# filename even when the checkout differs from the version currently indexed.
|
||||
package_version_is_published() {
|
||||
local repo_dir="$1" package="$2" version="$3" target="$4" path
|
||||
for path in "$repo_dir/$package-$version-$target.pkg.tar."* \
|
||||
"$repo_dir/$package-$version-any.pkg.tar."*; do
|
||||
[[ -f "$path" && "$path" != *.sig ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Channel membership: where a package may be published. Packages without a
|
||||
# `channels` key are members of every channel (they flow edge -> rc -> stable).
|
||||
package_has_channels() {
|
||||
@@ -216,6 +280,11 @@ package_builds_for_mirror() {
|
||||
package_moves_to_channel() {
|
||||
local pkgdir="$1" channel="$2"
|
||||
package_in_channel "$pkgdir" "$channel" || return 1
|
||||
# Pinned packages build natively in rc from the release pin. That the
|
||||
# advancing environment lacks OMARCHY_RC_PINS (so *it* may not build them)
|
||||
# does not make the edge copy movable over the pin's artifact — edge's
|
||||
# version can be ahead of the in-flight RC.
|
||||
[[ "$channel" == "rc" ]] && package_is_pinned "$pkgdir" && return 1
|
||||
! package_builds_for_mirror "$pkgdir" "$channel"
|
||||
}
|
||||
|
||||
@@ -259,6 +328,31 @@ packages_for_upstream_sync() {
|
||||
done
|
||||
}
|
||||
|
||||
# Upstream updates travel in one of two lanes. The reviewed lane is the
|
||||
# 6-hourly sync PR a maintainer reads before merging. A package that marks
|
||||
# "auto_merge": true rides the unattended lane instead: its bump PR is opened
|
||||
# and auto-merged by the branch tracker as soon as CI is green, which is how a
|
||||
# package that follows a moving branch (omarchy-dev, omasnap-git) gets rebuilt
|
||||
# without anyone clicking. The lanes are disjoint so a branch tip can never
|
||||
# hold up a reviewed vendor release, or the other way round.
|
||||
package_auto_merge() {
|
||||
local pkgdir="$1" metadata
|
||||
metadata=$(metadata_file_for_dir "$pkgdir")
|
||||
[[ -f "$metadata" ]] || return 1
|
||||
[[ "$(jq -r 'if has("auto_merge") then .auto_merge else false end' "$metadata")" == "true" ]]
|
||||
}
|
||||
|
||||
# package_in_lane <pkgdir> <reviewed|auto-merge|all>
|
||||
package_in_lane() {
|
||||
local pkgdir="$1" lane="$2"
|
||||
case "$lane" in
|
||||
all | "") return 0 ;;
|
||||
auto-merge) package_auto_merge "$pkgdir" ;;
|
||||
reviewed) ! package_auto_merge "$pkgdir" ;;
|
||||
*) echo "invalid lane: $lane (expected reviewed, auto-merge, or all)" >&2; return 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Packages that must be rebuilt when a dependency they link against changes,
|
||||
# even though nothing in their own source moved. `rebuild_on` names those
|
||||
# dependencies; `rebuilt_against` records the versions the checked-in pkgrel was
|
||||
@@ -298,9 +392,12 @@ packages_for_mirror() {
|
||||
|
||||
packages_for_unscoped_build() {
|
||||
local mirror="$1"
|
||||
local arch="${2:-${ARCH:-x86_64}}"
|
||||
|
||||
package_dirs | while IFS= read -r pkgdir; do
|
||||
if package_builds_for_mirror "$pkgdir" "$mirror" && ! package_build_skipped "$pkgdir"; then
|
||||
if package_builds_for_mirror "$pkgdir" "$mirror" &&
|
||||
! package_build_skipped "$pkgdir" &&
|
||||
package_supports_arch "$pkgdir" "$arch"; then
|
||||
basename "$pkgdir"
|
||||
fi
|
||||
done
|
||||
@@ -443,23 +540,70 @@ validate_package_metadata() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! jq -e 'if has("auto_merge") | not then true else (.auto_merge | type) == "boolean" end' "$metadata" >/dev/null; then
|
||||
echo "invalid auto_merge for $(basename "$pkgdir"): must be boolean"
|
||||
return 1
|
||||
fi
|
||||
if package_auto_merge "$pkgdir" && ! package_has_upstream_provider "$pkgdir" && ! package_has_upstream_hook "$pkgdir"; then
|
||||
echo "invalid auto_merge for $(basename "$pkgdir"): only an upstream watch, provider, or hook can be auto-merged"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# `has` rather than `// {}`: jq's // treats false as absent, which would
|
||||
# let "upstream": false slip through as an empty declaration.
|
||||
if ! jq -e '
|
||||
def valid_sources:
|
||||
type == "object" and length > 0 and (to_entries | all(
|
||||
(.key | test("\\A[a-z0-9_]+\\z"))
|
||||
and (.value | type == "array" and length > 0 and all(type == "string" and length > 0))
|
||||
));
|
||||
def valid_assets:
|
||||
type == "object" and length > 0 and (to_entries | all(
|
||||
(.key | test("\\A[a-z0-9_]+\\z"))
|
||||
and (.value |
|
||||
(type == "string" and length > 0)
|
||||
or (type == "array" and length > 0 and all(type == "string" and length > 0) and (unique | length) == length)
|
||||
)
|
||||
));
|
||||
if has("upstream") | not then true
|
||||
elif (.upstream | type) != "object" then false
|
||||
else .upstream |
|
||||
((.github // "") | type == "string" and test("\\A[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\\z"))
|
||||
and ((.checksums // "") | type == "string" and length > 0)
|
||||
and ((.assets // {}) | type == "object" and length > 0 and (to_entries | all(
|
||||
(.key | test("\\A[a-z0-9_]+\\z")) and (.value | type == "string" and length > 0)
|
||||
)))
|
||||
([has("github"), has("git_tags"), has("npm"), has("debian"), has("watch")] | map(select(.)) | length) == 1
|
||||
and if has("watch") then (.watch | type == "object")
|
||||
elif has("github") then
|
||||
(.github | type == "string" and test("\\A[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\\z"))
|
||||
and (if has("checksums") then (.checksums | type == "string" and length > 0) else true end)
|
||||
and (if has("digests") then (.digests | type == "boolean") else true end)
|
||||
and (if has("latest_only") then (.latest_only | type == "boolean") else true end)
|
||||
and (has("checksums") != (has("digests") and .digests == true))
|
||||
and (.assets | valid_assets)
|
||||
and (if has("sources") then
|
||||
(.sources | valid_sources)
|
||||
and ((.assets | keys) as $assets | (.sources | keys) as $sources | ($assets - $sources | length) == ($assets | length))
|
||||
else true end)
|
||||
elif has("git_tags") then
|
||||
(.git_tags | type == "string" and test("\\Ahttps://[^[:space:]]+\\.git\\z"))
|
||||
and (.tag_pattern | type == "string" and (split("{pkgver}") | length) == 2)
|
||||
and (.sources | valid_sources)
|
||||
elif has("npm") then
|
||||
(.npm | type == "string" and test("\\A(@[a-z0-9_.-]+/)?[a-z0-9_.-]+\\z"))
|
||||
and ((.dist_tag // "latest") | type == "string" and test("\\A[a-z0-9_.-]+\\z"))
|
||||
and (.sources | valid_sources)
|
||||
else
|
||||
(.debian | type == "string" and test("\\Ahttps://[^[:space:]]+\\z"))
|
||||
and (.package | type == "string" and test("\\A[a-z0-9][a-z0-9+.-]*\\z"))
|
||||
and (.sources | valid_sources)
|
||||
end
|
||||
end
|
||||
' "$metadata" >/dev/null; then
|
||||
echo "invalid upstream for $(basename "$pkgdir"): needs github owner/repo, checksums asset name, and an assets arch->name map"
|
||||
echo "invalid upstream for $(basename "$pkgdir"): configure exactly one valid github, git_tags, npm, debian, or watch provider"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if jq -e '.upstream? | objects | has("watch")' "$metadata" >/dev/null; then
|
||||
python3 "${BASH_SOURCE[0]%/*}/upstream-watch.py" validate "$pkgdir" || return 1
|
||||
fi
|
||||
|
||||
pkgrel_type=$(jq -r 'if has("pkgrel") then .pkgrel | type else "missing" end' "$metadata")
|
||||
case "$pkgrel_type" in
|
||||
object|missing) ;;
|
||||
@@ -481,12 +625,28 @@ validate_package_metadata() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! jq -e '(.rebuilt_against // {}) | type == "object" and (to_entries | all(.value | type == "string" and length > 0))' "$metadata" >/dev/null; then
|
||||
echo "invalid rebuilt_against for $(basename "$pkgdir"): must be an object mapping package names to versions"
|
||||
if ! jq -e '
|
||||
def version_map:
|
||||
type == "object" and (to_entries | all(.value | type == "string" and length > 0));
|
||||
(.rebuilt_against // {}) as $record |
|
||||
($record | version_map) or
|
||||
(($record | type) == "object"
|
||||
and ((($record | keys) - ["x86_64", "aarch64"]) | length == 0)
|
||||
and ($record | to_entries | all(.value | version_map)))
|
||||
' "$metadata" >/dev/null; then
|
||||
echo "invalid rebuilt_against for $(basename "$pkgdir"): must map architectures to package-version maps"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! jq -e '((.rebuilt_against // {}) | keys) - (.rebuild_on // []) | length == 0' "$metadata" >/dev/null; then
|
||||
if ! jq -e '
|
||||
(.rebuild_on // []) as $triggers |
|
||||
(.rebuilt_against // {}) as $record |
|
||||
if ($record | to_entries | all(.value | type == "string")) then
|
||||
((($record | keys) - $triggers) | length == 0)
|
||||
else
|
||||
($record | to_entries | all((((.value | keys) - $triggers) | length) == 0))
|
||||
end
|
||||
' "$metadata" >/dev/null; then
|
||||
echo "invalid rebuilt_against for $(basename "$pkgdir"): records a package that rebuild_on does not name"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -5,6 +5,76 @@
|
||||
ARCH=${ARCH:-x86_64}
|
||||
MIRROR=${MIRROR:-edge}
|
||||
|
||||
# Architectures the tooling knows how to build.
|
||||
VALID_ARCHES="x86_64 aarch64"
|
||||
|
||||
# Architectures this repository PUBLISHES. The scheduled pipeline (version
|
||||
# check, auto-release, channel advance with --arch all) runs once per entry,
|
||||
# in this order; the first entry is the reference architecture that the
|
||||
# release train observes channels through. Adding an architecture here is the
|
||||
# enablement step: the next check-versions tick queues its packages and the
|
||||
# next auto-release tick builds them. OMARCHY_ARCHES overrides it for a
|
||||
# one-off run.
|
||||
PUBLISHED_ARCHES="${OMARCHY_ARCHES:-x86_64}"
|
||||
|
||||
validate_arch() {
|
||||
case " $VALID_ARCHES " in
|
||||
*" $1 "*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
require_valid_arch() {
|
||||
if ! validate_arch "$1"; then
|
||||
echo "Invalid architecture: $1 (must be one of: $VALID_ARCHES)" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
published_arches() {
|
||||
local arch
|
||||
for arch in $PUBLISHED_ARCHES; do
|
||||
require_valid_arch "$arch"
|
||||
echo "$arch"
|
||||
done
|
||||
}
|
||||
|
||||
reference_arch() {
|
||||
published_arches | head -1
|
||||
}
|
||||
|
||||
# Architectures a merge to master builds and publishes: bin/build-matrix plans
|
||||
# a PR build for each one a package supports, and publish.yml ships them. This
|
||||
# is independent of PUBLISHED_ARCHES, so anything deciding what a merge has to
|
||||
# rebuild (bin/sync-rebuilds) follows this list. CI_ARCHES overrides it.
|
||||
CI_ARCHES="${CI_ARCHES:-x86_64 aarch64}"
|
||||
|
||||
ci_arches() {
|
||||
local arch
|
||||
for arch in $CI_ARCHES; do
|
||||
require_valid_arch "$arch"
|
||||
echo "$arch"
|
||||
done
|
||||
}
|
||||
|
||||
# Scheduled-pipeline state, one file per channel and architecture, so one
|
||||
# architecture's queue or backoff never gates another's.
|
||||
STATE_DIR="${OMARCHY_STATE_DIR:-/root/.state}"
|
||||
|
||||
sync_queue_file() { # sync_queue_file <mirror> <arch>
|
||||
echo "$STATE_DIR/.sync-needed-$1-$2"
|
||||
}
|
||||
|
||||
sync_fail_file() { # sync_fail_file <mirror> <arch>
|
||||
echo "$STATE_DIR/.build-failed-$1-$2"
|
||||
}
|
||||
|
||||
# The pre-architecture names, .sync-needed-<mirror> and .build-failed-<mirror>,
|
||||
# meant x86_64. A host upgraded mid-cycle may still hold one; readers treat
|
||||
# it as the x86_64 file until it is consumed.
|
||||
legacy_sync_queue_file() { echo "$STATE_DIR/.sync-needed-$1"; }
|
||||
legacy_sync_fail_file() { echo "$STATE_DIR/.build-failed-$1"; }
|
||||
|
||||
# Valid package channels, in pipeline order: packages move edge -> rc -> stable
|
||||
VALID_MIRRORS="edge rc stable"
|
||||
|
||||
|
||||
+321
-32
@@ -1,8 +1,7 @@
|
||||
# GitHub-releases upstream provider for bin/sync-upstream.
|
||||
# Declarative upstream providers for bin/sync-upstream.
|
||||
#
|
||||
# A package whose upstream ships tagged GitHub releases with a checksum
|
||||
# manifest asset needs no upstream.sh hook: the whole feed is data, declared
|
||||
# in .omarchy/package.json --
|
||||
# A package whose upstream ships tagged GitHub releases needs no upstream.sh
|
||||
# hook: the whole feed is data, declared in .omarchy/package.json --
|
||||
#
|
||||
# "upstream": {
|
||||
# "github": "jdx/mise",
|
||||
@@ -13,17 +12,28 @@
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# "checksums" names the vendor's manifest asset. A vendor publishing none can
|
||||
# set "digests": true instead, which reads the SHA-256 digest GitHub's release
|
||||
# API reports for every asset, so the sync never downloads the artifacts.
|
||||
#
|
||||
# {tag} and {pkgver} interpolate into asset names; tags may carry a leading
|
||||
# "v", which is stripped for pkgver. Drafts and prereleases are ignored. The
|
||||
# provider emits the same JSON contract as an upstream.sh hook, so
|
||||
# bin/sync-upstream's validation and min_release_age backstop apply
|
||||
# unchanged; a feed that fits no convention keeps a bespoke upstream.sh.
|
||||
# unchanged. Git-tag, npm, and Debian Packages providers below cover projects
|
||||
# without GitHub releases; a feed that fits no convention keeps a bespoke hook.
|
||||
|
||||
package_upstream_github_repo() {
|
||||
local pkgdir="$1"
|
||||
# `objects` drops a non-object upstream value (validation rejects those
|
||||
# separately) instead of erroring the jq pipeline.
|
||||
package_metadata_value "$pkgdir" '(.upstream? | objects | .github)' ""
|
||||
# Return the single declarative provider selected by a package. An empty
|
||||
# result means either no provider or an invalid/ambiguous declaration; the
|
||||
# caller distinguishes those through package_has_upstream_provider().
|
||||
package_upstream_provider() {
|
||||
local pkgdir="$1" metadata
|
||||
metadata=$(metadata_file_for_dir "$pkgdir")
|
||||
jq -r '
|
||||
(.upstream? | objects) as $u
|
||||
| [$u | keys[] | select(. == "github" or . == "git_tags" or . == "npm" or . == "debian" or . == "watch")]
|
||||
| if length == 1 then .[0] else "" end
|
||||
' "$metadata"
|
||||
}
|
||||
|
||||
# Fetches sit behind functions so the self-test can replace them with fixture
|
||||
@@ -43,6 +53,221 @@ github_fetch_checksums() {
|
||||
curl -fsSL "https://github.com/$repo/releases/download/$tag/$asset"
|
||||
}
|
||||
|
||||
git_tags_fetch_refs() {
|
||||
local repo="$1"
|
||||
git ls-remote --tags "$repo"
|
||||
}
|
||||
|
||||
npm_fetch_metadata() {
|
||||
local package="$1" encoded
|
||||
encoded=$(jq -rn --arg package "$package" '$package | @uri')
|
||||
curl -fsSL "https://registry.npmjs.org/$encoded"
|
||||
}
|
||||
|
||||
debian_fetch_packages() {
|
||||
local url="$1"
|
||||
curl --proto '=https' --proto-redir '=https' -fsSL "$url"
|
||||
}
|
||||
|
||||
upstream_fetch_source() {
|
||||
local url="$1" output="$2"
|
||||
curl --proto '=https' --proto-redir '=https' -fsSL -o "$output" "$url"
|
||||
}
|
||||
|
||||
# Hash every URL template in upstream.sources and emit hook-contract JSON.
|
||||
# Templates may use {pkgver}, {tag}, and (for npm) {npm_tarball}. Downloads
|
||||
# happen only after discovery reports a version newer than the PKGBUILD.
|
||||
upstream_hash_sources() {
|
||||
local package_dir="$1" pkgver="$2" tag="${3:-}" npm_tarball="${4:-}"
|
||||
local metadata sources work result arch template url file sum sums index=0
|
||||
metadata=$(metadata_file_for_dir "$package_dir")
|
||||
sources=$(jq -c '.upstream.sources' "$metadata")
|
||||
work=$(mktemp -d)
|
||||
result=$(jq -n --arg pkgver "$pkgver" '{pkgver: $pkgver, sha256sums: {}}')
|
||||
|
||||
while IFS= read -r arch; do
|
||||
sums='[]'
|
||||
while IFS= read -r template; do
|
||||
if [[ "$template" == file:* ]]; then
|
||||
file=${template#file:}
|
||||
if [[ ! "$file" =~ ^[A-Za-z0-9._+-]+$ || ! -f "$package_dir/$file" ]]; then
|
||||
echo "upstream source names an unsafe or missing local file: '$file'" >&2
|
||||
rm -rf "$work"
|
||||
return 1
|
||||
fi
|
||||
sum=$(sha256sum "$package_dir/$file" | cut -d' ' -f1)
|
||||
sums=$(jq -c --arg sum "$sum" '. + [$sum]' <<<"$sums")
|
||||
continue
|
||||
fi
|
||||
url=${template//\{pkgver\}/$pkgver}
|
||||
url=${url//\{tag\}/$tag}
|
||||
url=${url//\{npm_tarball\}/$npm_tarball}
|
||||
if [[ ! "$url" =~ ^https://[^[:space:]{}]+$ ]]; then
|
||||
echo "upstream source template produced an unsafe URL: '$url'" >&2
|
||||
rm -rf "$work"
|
||||
return 1
|
||||
fi
|
||||
file="$work/source-$((index += 1))"
|
||||
if ! upstream_fetch_source "$url" "$file"; then
|
||||
echo "could not fetch upstream source: $url" >&2
|
||||
rm -rf "$work"
|
||||
return 1
|
||||
fi
|
||||
sum=$(sha256sum "$file" | cut -d' ' -f1)
|
||||
sums=$(jq -c --arg sum "$sum" '. + [$sum]' <<<"$sums")
|
||||
done < <(jq -r --arg arch "$arch" '.[$arch][]' <<<"$sources")
|
||||
result=$(jq -c --arg arch "$arch" --argjson sums "$sums" '.sha256sums[$arch] = $sums' <<<"$result")
|
||||
done < <(jq -r 'keys[]' <<<"$sources")
|
||||
|
||||
rm -rf "$work"
|
||||
printf '%s\n' "$result"
|
||||
}
|
||||
|
||||
git_tags_upstream_release() {
|
||||
local package_dir="$1" metadata repo pattern prefix suffix refs
|
||||
metadata=$(metadata_file_for_dir "$package_dir")
|
||||
repo=$(jq -r '.upstream.git_tags // ""' "$metadata")
|
||||
pattern=$(jq -r '.upstream.tag_pattern // ""' "$metadata")
|
||||
prefix=${pattern%%\{pkgver\}*}
|
||||
suffix=${pattern#*\{pkgver\}}
|
||||
|
||||
if [[ ! "$repo" =~ ^https://[^[:space:]]+\.git$ || "$pattern" != *'{pkgver}'* || "$suffix" == *'{pkgver}'* ]]; then
|
||||
echo "invalid git_tags provider configuration" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! refs=$(git_tags_fetch_refs "$repo"); then
|
||||
echo "could not fetch tags from $repo" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local best_pkgver="" best_tag="" tag candidate
|
||||
declare -A version_tags=()
|
||||
while read -r tag; do
|
||||
tag=${tag%\^\{\}}
|
||||
[[ "$tag" == "$prefix"*"$suffix" ]] || continue
|
||||
candidate=${tag#"$prefix"}
|
||||
[[ -z "$suffix" ]] || candidate=${candidate%"$suffix"}
|
||||
[[ "$candidate" =~ ^[A-Za-z0-9][A-Za-z0-9._+]*$ ]] || continue
|
||||
if [[ -n "${version_tags[$candidate]:-}" && "${version_tags[$candidate]}" != "$tag" ]]; then
|
||||
echo "multiple tags map to pkgver $candidate: ${version_tags[$candidate]} and $tag" >&2
|
||||
return 1
|
||||
fi
|
||||
version_tags[$candidate]="$tag"
|
||||
if [[ -z "$best_pkgver" || $(vercmp "$candidate" "$best_pkgver") -gt 0 ]]; then
|
||||
best_pkgver="$candidate"
|
||||
best_tag="$tag"
|
||||
fi
|
||||
done < <(sed -n 's#^.*refs/tags/##p' <<<"$refs")
|
||||
|
||||
[[ -n "$best_pkgver" ]] || { echo "no usable tags found at $repo" >&2; return 1; }
|
||||
local current_pkgver
|
||||
current_pkgver=$(grep -m1 '^pkgver=' "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'")
|
||||
if [[ $(vercmp "$best_pkgver" "$current_pkgver") -le 0 ]]; then
|
||||
echo '{}'
|
||||
return 0
|
||||
fi
|
||||
upstream_hash_sources "$package_dir" "$best_pkgver" "$best_tag"
|
||||
}
|
||||
|
||||
npm_upstream_release() {
|
||||
local package_dir="$1" metadata package dist_tag npm_metadata pkgver tarball published_at release
|
||||
metadata=$(metadata_file_for_dir "$package_dir")
|
||||
package=$(jq -r '.upstream.npm // ""' "$metadata")
|
||||
dist_tag=$(jq -r '.upstream.dist_tag // "latest"' "$metadata")
|
||||
if [[ ! "$package" =~ ^(@[a-z0-9_.-]+/)?[a-z0-9_.-]+$ || ! "$dist_tag" =~ ^[a-z0-9_.-]+$ ]]; then
|
||||
echo "invalid npm provider configuration" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! npm_metadata=$(npm_fetch_metadata "$package"); then
|
||||
echo "could not fetch npm metadata for $package" >&2
|
||||
return 1
|
||||
fi
|
||||
pkgver=$(jq -r --arg tag "$dist_tag" '."dist-tags"[$tag] // ""' <<<"$npm_metadata")
|
||||
tarball=$(jq -r --arg version "$pkgver" '.versions[$version].dist.tarball // ""' <<<"$npm_metadata")
|
||||
published_at=$(jq -r --arg version "$pkgver" '.time[$version] // ""' <<<"$npm_metadata")
|
||||
if [[ ! "$pkgver" =~ ^[A-Za-z0-9][A-Za-z0-9._+]*$ || ! "$tarball" =~ ^https://registry\.npmjs\.org/ ]]; then
|
||||
echo "npm returned an unusable $package release" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local current_pkgver
|
||||
current_pkgver=$(grep -m1 '^pkgver=' "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'")
|
||||
if [[ $(vercmp "$pkgver" "$current_pkgver") -le 0 ]]; then
|
||||
echo '{}'
|
||||
return 0
|
||||
fi
|
||||
release=$(upstream_hash_sources "$package_dir" "$pkgver" "$pkgver" "$tarball") || return 1
|
||||
if [[ -n "$published_at" ]]; then
|
||||
release=$(jq -c --arg published_at "$published_at" '.published_at = $published_at' <<<"$release")
|
||||
fi
|
||||
printf '%s\n' "$release"
|
||||
}
|
||||
|
||||
# Discover a package version from a plain-text Debian Packages index, then
|
||||
# hash the declared immutable sources. This intentionally supports only
|
||||
# versions that are already valid Arch pkgver values; feeds needing Debian
|
||||
# epoch/revision translation keep a package-specific hook.
|
||||
debian_upstream_release() {
|
||||
local package_dir="$1" metadata index_url package packages
|
||||
metadata=$(metadata_file_for_dir "$package_dir")
|
||||
index_url=$(jq -r '.upstream.debian // ""' "$metadata")
|
||||
package=$(jq -r '.upstream.package // ""' "$metadata")
|
||||
|
||||
if [[ ! "$index_url" =~ ^https://[^[:space:]]+$ || ! "$package" =~ ^[a-z0-9][a-z0-9+.-]*$ ]]; then
|
||||
echo "invalid Debian Packages provider configuration" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! jq -e '
|
||||
.upstream.sources | type == "object" and length > 0 and (to_entries | all(
|
||||
(.key | test("\\A[a-z0-9_]+\\z"))
|
||||
and (.value | type == "array" and length > 0 and all(type == "string" and length > 0))
|
||||
))
|
||||
' "$metadata" >/dev/null; then
|
||||
echo "invalid Debian Packages source mapping" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! packages=$(debian_fetch_packages "$index_url"); then
|
||||
echo "could not fetch Debian Packages index: $index_url" >&2
|
||||
return 1
|
||||
fi
|
||||
packages=${packages//$'\r'/}
|
||||
|
||||
local best_pkgver="" candidate
|
||||
while IFS= read -r candidate; do
|
||||
if [[ ! "$candidate" =~ ^[A-Za-z0-9][A-Za-z0-9._+]*$ ]]; then
|
||||
echo "$package has an unusable Debian version: ${candidate:-<empty>}" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ -z "$best_pkgver" || $(vercmp "$candidate" "$best_pkgver") -gt 0 ]]; then
|
||||
best_pkgver="$candidate"
|
||||
fi
|
||||
done < <(awk -v target="$package" '
|
||||
BEGIN { RS = ""; FS = "\n" }
|
||||
{
|
||||
name = version = ""
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i ~ /^Package: /) name = substr($i, 10)
|
||||
if ($i ~ /^Version: /) version = substr($i, 10)
|
||||
}
|
||||
if (name == target) print version
|
||||
}
|
||||
' <<<"$packages")
|
||||
|
||||
[[ -n "$best_pkgver" ]] || {
|
||||
echo "no usable $package release found in $index_url" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
local current_pkgver
|
||||
current_pkgver=$(grep -m1 '^pkgver=' "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'")
|
||||
if [[ $(vercmp "$best_pkgver" "$current_pkgver") -le 0 ]]; then
|
||||
echo '{}'
|
||||
return 0
|
||||
fi
|
||||
|
||||
upstream_hash_sources "$package_dir" "$best_pkgver" "$best_pkgver"
|
||||
}
|
||||
|
||||
# Emits the newest qualifying release as hook-contract JSON. min_release_age
|
||||
# is honored during selection (newest release older than the window wins,
|
||||
# even when a younger one exists) and BYPASS_MIN_RELEASE_AGE=1 lifts it.
|
||||
@@ -51,7 +276,7 @@ github_fetch_checksums() {
|
||||
# not silently choose from.
|
||||
github_upstream_release() {
|
||||
local package_dir="$1" min_age="${2:-0}"
|
||||
local metadata repo checksums_name
|
||||
local metadata repo checksums_name use_digests latest_only
|
||||
metadata=$(metadata_file_for_dir "$package_dir")
|
||||
|
||||
repo=$(jq -r '(.upstream? | objects | .github) // ""' "$metadata")
|
||||
@@ -59,9 +284,46 @@ github_upstream_release() {
|
||||
echo "invalid upstream.github repository: '${repo:-<empty>}'" >&2
|
||||
return 1
|
||||
fi
|
||||
checksums_name=$(jq -r '(.upstream? | objects | .checksums) // ""' "$metadata")
|
||||
if [[ -z "$checksums_name" ]]; then
|
||||
echo "upstream.checksums names the checksum manifest asset and is required" >&2
|
||||
# Enforced here as well as in validate_package_metadata: the scheduled sync
|
||||
# reaches this provider without running the validator first.
|
||||
checksums_name=$(jq -r '(.upstream? | objects | .checksums) | strings' "$metadata")
|
||||
use_digests=$(jq -r '(.upstream? | objects | .digests) | if . == null then "false" elif type == "boolean" then tostring else "invalid" end' "$metadata")
|
||||
latest_only=$(jq -r '(.upstream? | objects | .latest_only) | if . == null then "false" elif type == "boolean" then tostring else "invalid" end' "$metadata")
|
||||
if [[ "$use_digests" == "invalid" ]]; then
|
||||
echo "upstream.digests must be true or false" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ "$latest_only" == "invalid" ]]; then
|
||||
echo "upstream.latest_only must be true or false" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ -n "$checksums_name" && "$use_digests" == "true" ]]; then
|
||||
echo "upstream sets both checksums and digests; keep exactly one" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ -z "$checksums_name" && "$use_digests" != "true" ]]; then
|
||||
echo "upstream needs either checksums (a manifest asset name) or digests: true" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! jq -e '
|
||||
def valid_sources:
|
||||
type == "object" and length > 0 and (to_entries | all(
|
||||
(.key | test("\\A[a-z0-9_]+\\z"))
|
||||
and (.value | type == "array" and length > 0 and all(type == "string" and length > 0))
|
||||
));
|
||||
(.upstream.assets | type == "object" and length > 0 and (to_entries | all(
|
||||
(.key | test("\\A[a-z0-9_]+\\z"))
|
||||
and (.value |
|
||||
(type == "string" and length > 0)
|
||||
or (type == "array" and length > 0 and all(type == "string" and length > 0) and (unique | length) == length)
|
||||
)
|
||||
)))
|
||||
and (if .upstream | has("sources") then
|
||||
(.upstream.sources | valid_sources)
|
||||
and ((.upstream.assets | keys) as $assets | (.upstream.sources | keys) as $sources | ($assets - $sources | length) == ($assets | length))
|
||||
else true end)
|
||||
' "$metadata" >/dev/null; then
|
||||
echo "invalid upstream.assets or upstream.sources mapping" >&2
|
||||
return 1
|
||||
fi
|
||||
local arches
|
||||
@@ -77,6 +339,9 @@ github_upstream_release() {
|
||||
echo "could not fetch the release feed for $repo" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ "$latest_only" == "true" ]]; then
|
||||
releases=$(jq '[.[] | select((.draft or .prerelease) | not)][0:1]' <<<"$releases")
|
||||
fi
|
||||
|
||||
local candidates=0 best_tag="" best_pkgver="" best_published_at=""
|
||||
local tag published_at pkgver published_epoch
|
||||
@@ -129,33 +394,57 @@ github_upstream_release() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
local checksums
|
||||
if ! checksums=$(github_fetch_checksums "$repo" "$best_tag" "$checksums_name"); then
|
||||
local checksums=""
|
||||
if [[ "$use_digests" != "true" ]] \
|
||||
&& ! checksums=$(github_fetch_checksums "$repo" "$best_tag" "$checksums_name"); then
|
||||
echo "could not fetch $checksums_name for $repo $best_tag" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local jq_args=(--arg pkgver "$best_pkgver" --arg published_at "$best_published_at")
|
||||
local jq_filter='{pkgver: $pkgver, published_at: $published_at, sha256sums: {}}'
|
||||
local arch template filename checksum
|
||||
local result
|
||||
result=$(jq -n --arg pkgver "$best_pkgver" --arg published_at "$best_published_at" \
|
||||
'{pkgver: $pkgver, published_at: $published_at, sha256sums: {}}')
|
||||
local arch template filename checksum checksum_source sums
|
||||
for arch in "${arches[@]}"; do
|
||||
if [[ ! "$arch" =~ ^[a-z0-9_]+$ ]]; then
|
||||
echo "invalid architecture key in upstream.assets: '$arch'" >&2
|
||||
return 1
|
||||
fi
|
||||
template=$(jq -r --arg arch "$arch" '.upstream.assets[$arch]' "$metadata")
|
||||
filename=${template//\{pkgver\}/$best_pkgver}
|
||||
filename=${filename//\{tag\}/$best_tag}
|
||||
# Manifest lines are "<sha256> <name>", with the name sometimes prefixed
|
||||
# "./" (sha256sum of a local path) or "*" (binary-mode marker).
|
||||
checksum=$(awk -v f="$filename" '$2 == f || $2 == "./" f || $2 == "*" f { print $1; exit }' <<<"$checksums")
|
||||
if [[ ! "$checksum" =~ ^[0-9a-f]{64}$ ]]; then
|
||||
echo "no valid checksum for $filename in $repo $best_tag $checksums_name" >&2
|
||||
return 1
|
||||
fi
|
||||
jq_args+=(--arg "sum_$arch" "$checksum")
|
||||
jq_filter+=" | .sha256sums[\"$arch\"] = [\$sum_$arch]"
|
||||
sums='[]'
|
||||
while IFS= read -r template; do
|
||||
filename=${template//\{pkgver\}/$best_pkgver}
|
||||
filename=${filename//\{tag\}/$best_tag}
|
||||
if [[ "$use_digests" == "true" ]]; then
|
||||
# Only a "sha256:<hex>" digest is stripped to its hex; any other shape
|
||||
# falls through empty and fails the check below.
|
||||
checksum=$(jq -r --arg tag "$best_tag" --arg name "$filename" '
|
||||
first(.[] | select(.tag_name == $tag)) | (.assets // [])[]
|
||||
| select(.name == $name) | (.digest // "")
|
||||
| if type == "string" and test("\\Asha256:[0-9a-f]{64}\\z") then ltrimstr("sha256:") else "" end
|
||||
' <<<"$releases")
|
||||
checksum_source="the release API digest"
|
||||
else
|
||||
# Manifest lines are "<sha256> <name>", with the name sometimes prefixed
|
||||
# "./" (sha256sum of a local path) or "*" (binary-mode marker).
|
||||
checksum=$(awk -v f="$filename" '$2 == f || $2 == "./" f || $2 == "*" f { print $1; exit }' <<<"$checksums")
|
||||
checksum_source="$checksums_name"
|
||||
fi
|
||||
if [[ ! "$checksum" =~ ^[0-9a-f]{64}$ ]]; then
|
||||
echo "no valid checksum for $filename in $repo $best_tag $checksum_source" >&2
|
||||
return 1
|
||||
fi
|
||||
sums=$(jq -c --arg checksum "$checksum" '. + [$checksum]' <<<"$sums")
|
||||
done < <(jq -r --arg arch "$arch" '
|
||||
.upstream.assets[$arch] | if type == "array" then .[] else . end
|
||||
' "$metadata")
|
||||
result=$(jq -c --arg arch "$arch" --argjson sums "$sums" '.sha256sums[$arch] = $sums' <<<"$result")
|
||||
done
|
||||
|
||||
jq -n "${jq_args[@]}" "$jq_filter"
|
||||
if jq -e '.upstream | has("sources")' "$metadata" >/dev/null; then
|
||||
local source_release
|
||||
source_release=$(upstream_hash_sources "$package_dir" "$best_pkgver" "$best_tag") || return 1
|
||||
result=$(jq -c --argjson source "$source_release" '.sha256sums += $source.sha256sums' <<<"$result")
|
||||
fi
|
||||
|
||||
printf '%s\n' "$result"
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Discover releases and update our own recipes; never import upstream build code.
|
||||
|
||||
The watch selects release metadata. Sources, supported architectures, integrity
|
||||
algorithms and packaging behavior stay in the checked-in PKGBUILD. Only release
|
||||
scalars and checksum arrays are replaced, atomically, after every source passes.
|
||||
"""
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
from urllib.parse import quote, urlsplit
|
||||
import zipfile
|
||||
|
||||
PROVIDERS = {"github", "git_tags", "git_branch", "npm", "pypi", "debian", "json", "regex", "archive", "redirect"}
|
||||
VERSION = re.compile(r"[A-Za-z0-9][A-Za-z0-9._+]*\Z")
|
||||
SCALAR = re.compile(r"[A-Za-z0-9._+/-]+\Z")
|
||||
SUM = re.compile(r"(md5|sha1|sha224|sha256|sha384|sha512|b2)sums(_[a-z0-9_]+)?\Z")
|
||||
HASHES = {"b2": "blake2b"}
|
||||
# How many times a redirect watch probes before it calls the feed unmatched.
|
||||
REDIRECT_PROBES = 5
|
||||
|
||||
|
||||
def run(args, **kwargs):
|
||||
return subprocess.check_output(args, **kwargs)
|
||||
|
||||
|
||||
def vercmp(a, b):
|
||||
return int(run(["vercmp", a, b], text=True).strip())
|
||||
|
||||
|
||||
def https(url):
|
||||
parts = urlsplit(url)
|
||||
if parts.scheme != "https" or not parts.hostname or parts.username or parts.password or re.search(r"[\s\x00-\x1f]", url):
|
||||
raise ValueError(f"expected an HTTPS upstream URL: {url!r}")
|
||||
return url
|
||||
|
||||
|
||||
class Fetcher:
|
||||
def __init__(self, cache):
|
||||
self.cache = Path(cache)
|
||||
self.cache.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def file(self, url):
|
||||
https(url)
|
||||
dest = self.cache / hashlib.sha256(url.encode()).hexdigest()
|
||||
if not dest.exists():
|
||||
scratch = dest.with_suffix(f".{os.getpid()}.tmp")
|
||||
command = ["curl", "--proto", "=https", "--proto-redir", "=https", "-fsSL",
|
||||
"--connect-timeout", "20", "--max-time", "300", "--retry", "2", "-o", str(scratch), url]
|
||||
# Credentials only go to GitHub's API, never to release assets or vendors.
|
||||
token = os.environ.get("UPSTREAM_GITHUB_TOKEN")
|
||||
if token and urlsplit(url).hostname == "api.github.com":
|
||||
command[1:1] = ["--config", "-"]
|
||||
subprocess.run(command, input=f'header = "Authorization: Bearer {token}"\n', text=True, check=True)
|
||||
else:
|
||||
subprocess.run(command, check=True)
|
||||
scratch.replace(dest)
|
||||
return dest
|
||||
|
||||
def text(self, url):
|
||||
data = self.file(url).read_bytes()
|
||||
if data.startswith(b"\x1f\x8b"):
|
||||
data = gzip.decompress(data)
|
||||
return data.decode()
|
||||
|
||||
def json(self, url):
|
||||
return json.loads(self.text(url))
|
||||
|
||||
|
||||
def validate(watch):
|
||||
if not isinstance(watch, dict) or len(PROVIDERS & watch.keys()) != 1:
|
||||
raise ValueError("watch must select exactly one release provider")
|
||||
provider = next(iter(PROVIDERS & watch.keys()))
|
||||
allowed = PROVIDERS | {"pattern", "path", "package", "branch", "variables", "fields",
|
||||
"submodules", "allow_prerelease", "unescape_json", "filenames",
|
||||
"sequence", "version", "revision", "revision_variable",
|
||||
"mutable_sources", "member", "dist_tag", "tag_pattern"}
|
||||
if watch.keys() - allowed:
|
||||
raise ValueError(f"unknown watch fields: {sorted(watch.keys() - allowed)}")
|
||||
value = watch[provider]
|
||||
if not isinstance(value, str) or not value:
|
||||
raise ValueError(f"invalid watch.{provider}")
|
||||
if provider == "github":
|
||||
if not re.fullmatch(r"[\w.-]+/[\w.-]+", value):
|
||||
raise ValueError("invalid GitHub repository")
|
||||
elif provider in {"npm", "pypi"}:
|
||||
if not re.fullmatch(r"(?:@[\w.-]+/)?[\w.-]+", value):
|
||||
raise ValueError("invalid registry package")
|
||||
else:
|
||||
https(value)
|
||||
if "pattern" in watch or provider in {"github", "git_tags", "regex", "archive", "redirect"}:
|
||||
if not isinstance(watch.get("pattern"), str):
|
||||
raise ValueError("watch needs an explicit release pattern")
|
||||
pattern = re.compile(watch["pattern"])
|
||||
if "version" not in pattern.groupindex:
|
||||
raise ValueError("release pattern needs a named version group")
|
||||
if provider == "json" and (not isinstance(watch.get("path"), str) or not watch["path"]):
|
||||
raise ValueError("JSON watch needs a version path")
|
||||
if provider == "debian":
|
||||
name = watch.get("package", "")
|
||||
if not isinstance(name, str) or not re.fullmatch(r"[a-z0-9][a-z0-9+.-]*", name):
|
||||
raise ValueError("Debian watch needs an exact package name")
|
||||
if provider == "git_branch":
|
||||
branch = watch.get("branch", "")
|
||||
if not isinstance(branch, str) or not branch or branch.startswith("-"):
|
||||
raise ValueError("git branch watch needs an explicit branch")
|
||||
run(["git", "check-ref-format", "refs/heads/" + branch])
|
||||
# A branch watch whose version template names a tag needs to know
|
||||
# which tags count as releases; anything else is an untagged branch.
|
||||
if "tag_pattern" in watch:
|
||||
if not isinstance(watch["tag_pattern"], str) or not watch["tag_pattern"]:
|
||||
raise ValueError("watch.tag_pattern must be a regular expression string")
|
||||
if "version" not in re.compile(watch["tag_pattern"]).groupindex:
|
||||
raise ValueError("tag_pattern needs a named version group")
|
||||
template = watch.get("version", "{version}")
|
||||
if any(field in template for field in ("{tag", "{distance")) and "tag_pattern" not in watch:
|
||||
raise ValueError("a version built from {tag}/{distance} needs a tag_pattern")
|
||||
elif "tag_pattern" in watch:
|
||||
raise ValueError("tag_pattern only applies to git_branch watches")
|
||||
for field in ("variables", "submodules", "fields"):
|
||||
mapping = watch.get(field, {})
|
||||
if not isinstance(mapping, dict):
|
||||
raise ValueError(f"watch.{field} must be a string mapping")
|
||||
for name, value in mapping.items():
|
||||
pattern = r"[a-z][a-z0-9_]*" if field == "fields" else r"_[a-z][a-z0-9_]*"
|
||||
if not re.fullmatch(pattern, name) or not isinstance(value, str) or not value:
|
||||
raise ValueError(f"invalid watch.{field} mapping")
|
||||
if "submodules" in watch and provider != "github":
|
||||
raise ValueError("submodules require a GitHub watch")
|
||||
if watch.get("variables", {}).keys() & watch.get("submodules", {}).keys():
|
||||
raise ValueError("a release variable cannot also be a submodule")
|
||||
for path in watch.get("submodules", {}).values():
|
||||
if path.startswith("/") or any(part in {"", ".", ".."} for part in path.split("/")):
|
||||
raise ValueError("submodule path must be relative to the release repository")
|
||||
for field in ("allow_prerelease", "unescape_json", "filenames", "sequence"):
|
||||
if field in watch and not isinstance(watch[field], bool):
|
||||
raise ValueError(f"watch.{field} must be boolean")
|
||||
for field in ("version", "revision", "member", "dist_tag"):
|
||||
if field in watch and (not isinstance(watch[field], str) or not watch[field]):
|
||||
raise ValueError(f"watch.{field} must be a string template")
|
||||
if "revision_variable" in watch:
|
||||
name = watch["revision_variable"]
|
||||
if not isinstance(name, str) or name not in watch.get("variables", {}) or not watch.get("revision"):
|
||||
raise ValueError("revision_variable requires a declared variable and revision template")
|
||||
for field in ("mutable_sources",):
|
||||
entries = watch.get(field, [])
|
||||
if not isinstance(entries, list) or any(not isinstance(v, str) or not re.fullmatch(r"source(?:_[a-z0-9_]+)?:[0-9]+", v) for v in entries):
|
||||
raise ValueError(f"watch.{field} must name source-array:index entries")
|
||||
return provider
|
||||
|
||||
|
||||
def json_path(data, path):
|
||||
for key in path.split("."):
|
||||
data = data[int(key)] if isinstance(data, list) else data[key]
|
||||
return data
|
||||
|
||||
|
||||
def candidate(watch, values):
|
||||
values = {k: str(v) for k, v in values.items() if v is not None}
|
||||
version = watch.get("version", "{version}").format_map(values)
|
||||
if not VERSION.fullmatch(version):
|
||||
raise ValueError(f"unusable upstream version: {version!r}")
|
||||
revision = watch.get("revision", "").format_map(values)
|
||||
if revision and not re.fullmatch(r"[0-9]+", revision):
|
||||
raise ValueError("upstream release revision must be numeric")
|
||||
return {"pkgver": version, "values": values,
|
||||
"published_at": values.get("published_at"),
|
||||
"revision": revision}
|
||||
|
||||
|
||||
def matches(watch, text, extra=None, full=False):
|
||||
pattern = re.compile(watch["pattern"])
|
||||
found = [pattern.fullmatch(text)] if full else pattern.finditer(text)
|
||||
for match in found:
|
||||
if match:
|
||||
yield candidate(watch, {**(extra or {}), **match.groupdict()})
|
||||
|
||||
|
||||
def git_branch_tip(url, branch, tag_pattern, cache):
|
||||
"""Describe the current tip of an upstream branch:
|
||||
commit, total count, date, and with a tag_pattern
|
||||
the newest release tag reachable from it plus the distance from that tag,
|
||||
so a branch build can be versioned <tag>.r<n>.g<sha>, above the release it
|
||||
follows and below the next one, the way a pkgver() function would.
|
||||
|
||||
One blobless single-branch clone per (url, branch) per run, shared by
|
||||
every package that tracks it, so two recipes pinned from one clone always
|
||||
see the same commit. The clone is read with git only; nothing in it runs.
|
||||
select_release applies the age hold to this tip, without walking back
|
||||
into history (which could select a commit from a merged side branch).
|
||||
"""
|
||||
https(url)
|
||||
key = hashlib.sha256(f"{url}#{branch}".encode()).hexdigest()
|
||||
work = Path(cache) / f"{key}.branch.git"
|
||||
if not work.exists():
|
||||
scratch = work.with_name(f"{work.name}.{os.getpid()}.tmp")
|
||||
subprocess.run(["git", "clone", "--quiet", "--bare", "--filter=blob:none", "--single-branch", "--branch", branch, url, str(scratch)], check=True)
|
||||
scratch.replace(work)
|
||||
git = ["git", "-C", str(work)]
|
||||
commit = run([*git, "rev-parse", "HEAD"], text=True).strip()
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", commit):
|
||||
raise ValueError("branch tip is not a commit")
|
||||
count = run([*git, "rev-list", "--count", commit], text=True).strip()
|
||||
date = run([*git, "show", "-s", "--format=%cs", commit], text=True).strip().replace("-", "")
|
||||
timestamp = run([*git, "show", "-s", "--format=%cI", commit], text=True).strip()
|
||||
values = {"version": date, "date": date, "count": count, "commit": commit, "published_at": timestamp}
|
||||
if tag_pattern:
|
||||
pattern = re.compile(tag_pattern)
|
||||
best = None
|
||||
# Only tags in this commit's history count; a release cut on another
|
||||
# branch is not something this branch is "past".
|
||||
for tag in run([*git, "tag", "--merged", commit], text=True).split():
|
||||
match = pattern.fullmatch(tag)
|
||||
if not match:
|
||||
continue
|
||||
version = match.group("version")
|
||||
if best is None or vercmp(version, best[0]) > 0:
|
||||
best = (version, tag)
|
||||
if best is None:
|
||||
raise ValueError(f"no tag on {branch} matches {tag_pattern}")
|
||||
distance = run([*git, "rev-list", "--count", f"{best[1]}..{commit}"], text=True).strip()
|
||||
values.update({"tag": best[1], "version": best[0], "distance": distance})
|
||||
return values
|
||||
|
||||
|
||||
def discover(watch, fetch):
|
||||
provider = validate(watch)
|
||||
feed = watch[provider]
|
||||
results = []
|
||||
if provider == "github":
|
||||
releases = fetch.json(f"https://api.github.com/repos/{feed}/releases?per_page=100")
|
||||
if not isinstance(releases, list):
|
||||
raise ValueError("GitHub did not return a release list")
|
||||
for release in releases:
|
||||
if release.get("draft") or (release.get("prerelease") and not watch.get("allow_prerelease")):
|
||||
continue
|
||||
for item in matches(watch, release["tag_name"], {"tag": release["tag_name"], "published_at": release["published_at"]}, full=True):
|
||||
item["assets"] = release.get("assets", [])
|
||||
results.append(item)
|
||||
elif provider == "git_tags":
|
||||
refs = run(["git", "ls-remote", "--tags", feed], text=True)
|
||||
tags = {}
|
||||
for line in refs.splitlines():
|
||||
commit, ref = line.split()
|
||||
tag = ref.removeprefix("refs/tags/")
|
||||
if tag.endswith("^{}"):
|
||||
tags[tag[:-3]] = commit
|
||||
else:
|
||||
tags.setdefault(tag, commit)
|
||||
for tag, commit in tags.items():
|
||||
results.extend(matches(watch, tag, {"tag": tag, "commit": commit}, full=True))
|
||||
elif provider == "git_branch":
|
||||
tip = git_branch_tip(feed, watch["branch"], watch.get("tag_pattern"), fetch.cache)
|
||||
results.append(candidate(watch, tip))
|
||||
elif provider == "npm":
|
||||
data = fetch.json("https://registry.npmjs.org/" + quote(feed, safe=""))
|
||||
version = data["dist-tags"][watch.get("dist_tag", "latest")]
|
||||
results.append(candidate(watch, {"version": version, "published_at": data.get("time", {}).get(version)}))
|
||||
elif provider == "pypi":
|
||||
data = fetch.json(f"https://pypi.org/pypi/{feed}/json")
|
||||
version = data["info"]["version"]
|
||||
dates = [r["upload_time_iso_8601"] for r in data["releases"].get(version, []) if not r.get("yanked")]
|
||||
if not dates:
|
||||
raise ValueError("PyPI release has no unyanked files")
|
||||
results.append(candidate(watch, {"version": version, "published_at": max(dates)}))
|
||||
elif provider == "debian":
|
||||
for stanza in re.split(r"\n\s*\n", fetch.text(feed).replace("\r", "")):
|
||||
fields = dict(re.findall(r"^([A-Za-z0-9-]+): (.*)$", stanza, re.M))
|
||||
if fields.get("Package") != watch["package"]:
|
||||
continue
|
||||
if "pattern" in watch:
|
||||
results.extend(matches(watch, fields["Version"], full=True))
|
||||
else:
|
||||
results.append(candidate(watch, {"version": fields["Version"]}))
|
||||
elif provider == "json":
|
||||
data = fetch.json(feed)
|
||||
values = {"version": json_path(data, watch["path"])}
|
||||
values.update({name: json_path(data, path) for name, path in watch.get("fields", {}).items()})
|
||||
results.append(candidate(watch, values))
|
||||
elif provider == "redirect":
|
||||
# A download redirect can be a staged rollout: some requests land on a
|
||||
# newer build the pattern deliberately rejects (Dropbox, 2026-10-04:
|
||||
# 4 of 100 HEADs ended at 274.3.4801 instead of 272.4.3798), so one
|
||||
# probe that misses is not an answer. Keep the first one that matches.
|
||||
for _ in range(REDIRECT_PROBES):
|
||||
final_url = run(["curl", "--proto", "=https", "--proto-redir", "=https", "-fsSLI", "--max-time", "60", "--retry", "2",
|
||||
"-o", "/dev/null", "-w", "%{url_effective}", feed], text=True)
|
||||
results.extend(matches(watch, final_url))
|
||||
if results:
|
||||
break
|
||||
elif provider == "regex":
|
||||
text = fetch.text(feed)
|
||||
if watch.get("unescape_json"):
|
||||
text = text.replace('\\"', '"')
|
||||
results.extend(matches(watch, text))
|
||||
elif provider == "archive":
|
||||
file = fetch.file(feed)
|
||||
if zipfile.is_zipfile(file):
|
||||
with zipfile.ZipFile(file) as archive:
|
||||
names = archive.namelist()
|
||||
if watch.get("filenames"):
|
||||
results.extend(matches(watch, "\n".join(names)))
|
||||
for name in ([] if watch.get("filenames") else names):
|
||||
if re.fullmatch(watch.get("member", ".*"), name):
|
||||
results.extend(matches(watch, archive.read(name).decode()))
|
||||
elif file.read_bytes()[:8] == b"!<arch>\n":
|
||||
names = run(["bsdtar", "-tf", str(file)], text=True).splitlines()
|
||||
controls = [name for name in names if name.startswith("control.tar")]
|
||||
if len(controls) != 1:
|
||||
raise ValueError("deb does not contain exactly one control archive")
|
||||
data = run(["bsdtar", "-xOf", str(file), controls[0]])
|
||||
with tarfile.open(fileobj=io.BytesIO(data)) as archive:
|
||||
members = [m for m in archive if m.name.removeprefix("./") == "control"]
|
||||
if len(members) != 1:
|
||||
raise ValueError("deb control file is missing or ambiguous")
|
||||
results.extend(matches(watch, archive.extractfile(members[0]).read().decode()))
|
||||
else:
|
||||
with tarfile.open(file) as archive:
|
||||
for member in archive:
|
||||
if member.isfile() and re.fullmatch(watch.get("member", ".*"), member.name):
|
||||
results.extend(matches(watch, archive.extractfile(member).read().decode()))
|
||||
if not results:
|
||||
raise ValueError(f"no matching releases in {feed}")
|
||||
return results
|
||||
|
||||
|
||||
def select_release(releases, min_age=0, now=None, bypass=False):
|
||||
now = now or dt.datetime.now(dt.timezone.utc)
|
||||
best = None
|
||||
for release in releases:
|
||||
if min_age and not bypass:
|
||||
value = release.get("published_at")
|
||||
if not value or not re.fullmatch(r"\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:?\d\d)", value):
|
||||
raise ValueError("release age cannot be established")
|
||||
if (now - dt.datetime.fromisoformat(value.replace("Z", "+00:00"))).total_seconds() < min_age:
|
||||
continue
|
||||
order = vercmp(release["pkgver"], best["pkgver"]) if best else 1
|
||||
if best and order == 0:
|
||||
order = vercmp(release["revision"] or "0", best["revision"] or "0")
|
||||
if order > 0:
|
||||
best = release
|
||||
return best
|
||||
|
||||
|
||||
DUMP = r'''
|
||||
source "$1" >/dev/null || exit 1
|
||||
set +u
|
||||
for __watch_name in pkgver pkgrel epoch arch $(compgen -A variable | LC_ALL=C sort); do
|
||||
case "$__watch_name" in
|
||||
pkgver|pkgrel|epoch|arch|source|source_*|md5sums*|sha1sums*|sha224sums*|sha256sums*|sha384sums*|sha512sums*|b2sums*|_*)
|
||||
[[ $__watch_name == __watch_* ]] && continue
|
||||
declare -n __watch_value="$__watch_name"
|
||||
printf '%s\0' "$__watch_name" "${#__watch_value[@]}" "${__watch_value[@]}"
|
||||
unset -n __watch_value
|
||||
;;
|
||||
esac
|
||||
done
|
||||
'''
|
||||
|
||||
|
||||
def read_recipe(path, arch="x86_64"):
|
||||
with tempfile.TemporaryDirectory(prefix="recipe-read-") as work:
|
||||
env = {**os.environ, "CARCH": arch, "SRCDEST": work, "srcdir": work, "pkgdir": work}
|
||||
data = run(["bash", "-c", DUMP, "_", str(path.resolve())], cwd=path.parent, env=env).decode().split("\0")
|
||||
result = {}
|
||||
index = 0
|
||||
while index < len(data) - 1:
|
||||
name, size = data[index:index + 2]
|
||||
index += 2
|
||||
size = int(size)
|
||||
result[name] = data[index:index + size]
|
||||
index += size
|
||||
return result
|
||||
|
||||
|
||||
def scalar(recipe, name, default=""):
|
||||
return recipe.get(name, [default])[0] if recipe.get(name) else default
|
||||
|
||||
|
||||
def replace_scalar(text, name, value):
|
||||
if not SCALAR.fullmatch(value):
|
||||
raise ValueError(f"unsafe {name} value")
|
||||
pattern = re.compile(r"^" + re.escape(name) + r"=.*$", re.M)
|
||||
if len(pattern.findall(text)) != 1:
|
||||
raise ValueError(f"expected one top-level {name}= assignment")
|
||||
return pattern.sub(lambda _: f"{name}={value}", text)
|
||||
|
||||
|
||||
def replace_array(text, name, values):
|
||||
starts = list(re.finditer(r"^" + re.escape(name) + r"=\(", text, re.M))
|
||||
if len(starts) != 1:
|
||||
raise ValueError(f"expected one top-level {name}= array")
|
||||
start = starts[0]
|
||||
depth, quote_char, escaped, comment = 1, None, False, False
|
||||
for index in range(start.end(), len(text)):
|
||||
char = text[index]
|
||||
if comment:
|
||||
if char == "\n": comment = False
|
||||
elif escaped:
|
||||
escaped = False
|
||||
elif char == "\\" and quote_char != "'":
|
||||
escaped = True
|
||||
elif quote_char:
|
||||
if char == quote_char: quote_char = None
|
||||
elif char in "\"'": quote_char = char
|
||||
elif char == "#" and (index == 0 or text[index - 1].isspace()): comment = True
|
||||
elif char == "(": depth += 1
|
||||
elif char == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
replacement = name + "=(" + " ".join("'" + value + "'" for value in values) + ")"
|
||||
return text[:start.start()] + replacement + text[index + 1:]
|
||||
raise ValueError(f"unclosed {name} array")
|
||||
|
||||
|
||||
def bump_pkgrel(value):
|
||||
if not re.fullmatch(r"[0-9]+(?:\.[0-9]+)?", value):
|
||||
raise ValueError(f"invalid pkgrel: {value}")
|
||||
components = value.split(".")
|
||||
components[-1] = str(int(components[-1]) + 1)
|
||||
return ".".join(components)
|
||||
|
||||
|
||||
def complete_version(recipe):
|
||||
return f"{scalar(recipe, 'epoch', '0')}:{scalar(recipe, 'pkgver')}-{scalar(recipe, 'pkgrel')}"
|
||||
|
||||
|
||||
def hash_file(path, algorithm):
|
||||
with path.open("rb") as stream:
|
||||
return hashlib.file_digest(stream, HASHES.get(algorithm, algorithm)).hexdigest()
|
||||
|
||||
|
||||
def source_url(source):
|
||||
return source.split("::", 1)[-1]
|
||||
|
||||
|
||||
def git_source_file(url, cache):
|
||||
base, fragment = url.removeprefix("git+").split("#", 1)
|
||||
kind, ref = fragment.split("=", 1)
|
||||
https(base)
|
||||
if kind not in {"tag", "commit"} or (kind == "commit" and not re.fullmatch(r"[0-9a-f]{40}", ref)):
|
||||
raise ValueError("VCS sources must name an immutable commit or a checksummed tag")
|
||||
if kind == "tag":
|
||||
run(["git", "check-ref-format", "refs/tags/" + ref])
|
||||
dest = cache / (hashlib.sha256(url.encode()).hexdigest() + ".git.tar")
|
||||
if not dest.exists():
|
||||
with tempfile.TemporaryDirectory(prefix="upstream-source-", dir=cache) as work:
|
||||
subprocess.run(["git", "init", "--quiet", "--bare", work], check=True)
|
||||
subprocess.run(["git", "-C", work, "fetch", "--quiet", "--depth=1", base, "refs/tags/" + ref if kind == "tag" else ref], check=True)
|
||||
scratch = Path(work) / "source.tar"
|
||||
with scratch.open("wb") as output:
|
||||
subprocess.run(["git", "-c", "core.abbrev=no", "-C", work, "archive", "--format", "tar", "FETCH_HEAD"], stdout=output, check=True)
|
||||
# The cache must never retain partial archives after a git failure.
|
||||
scratch.replace(dest)
|
||||
return dest
|
||||
|
||||
|
||||
def updated_checksums(before, after, package, fetch, release, watch):
|
||||
arrays = {}
|
||||
source_names = {key for key in before if key == "source" or key.startswith("source_")}
|
||||
if source_names != {key for key in after if key == "source" or key.startswith("source_")}:
|
||||
raise ValueError("release changed the set of source architectures")
|
||||
for source_name in sorted(source_names):
|
||||
old_sources, sources = before[source_name], after[source_name]
|
||||
suffix = source_name.removeprefix("source")
|
||||
names = [name for name in before if SUM.fullmatch(name) and (SUM.fullmatch(name)[2] or "") == suffix]
|
||||
if not sources:
|
||||
continue
|
||||
if len(sources) != len(old_sources) or not names:
|
||||
raise ValueError(f"{source_name}: sources changed shape or have no checksums")
|
||||
for name in names:
|
||||
if len(before[name]) != len(sources):
|
||||
raise ValueError(f"{name}: source/checksum count mismatch")
|
||||
values = []
|
||||
algorithm = SUM.fullmatch(name)[1]
|
||||
for index, source in enumerate(sources):
|
||||
old = before[name][index]
|
||||
if source == old_sources[index] and f"{source_name}:{index}" not in watch.get("mutable_sources", []):
|
||||
values.append(old)
|
||||
continue
|
||||
url = source_url(source)
|
||||
# A release API digest can supply SHA256 without downloading a
|
||||
# large asset, but only when its exact declared URL matches.
|
||||
assets = [a for a in release.get("assets", []) if a.get("browser_download_url") == url]
|
||||
if algorithm == "sha256" and len(assets) == 1 and re.fullmatch(r"sha256:[0-9a-f]{64}", assets[0].get("digest") or ""):
|
||||
values.append("SKIP" if old == "SKIP" else assets[0]["digest"][7:])
|
||||
continue
|
||||
if url.startswith("git+https://"):
|
||||
file = git_source_file(url, fetch.cache)
|
||||
elif url.startswith("https://"):
|
||||
file = fetch.file(url)
|
||||
elif "://" not in url:
|
||||
file = (package / url).resolve()
|
||||
if not file.is_relative_to(package.resolve()) or not file.is_file():
|
||||
raise ValueError(f"unsafe local source: {url}")
|
||||
else:
|
||||
raise ValueError(f"unsupported source transport: {url}")
|
||||
# Preserve existing signature/prepare()-verified sources. Never
|
||||
# introduce SKIP; still fetch changed URLs to verify availability.
|
||||
values.append("SKIP" if old == "SKIP" else hash_file(file, algorithm))
|
||||
if values != before[name]:
|
||||
arrays[name] = values
|
||||
return arrays
|
||||
|
||||
|
||||
def resolve_release_fields(watch, release, fetch):
|
||||
values = release["values"].copy()
|
||||
if "github" in watch and any("{commit}" in value for value in watch.get("variables", {}).values()):
|
||||
ref = fetch.json(f"https://api.github.com/repos/{watch['github']}/git/ref/tags/{quote(values['tag'], safe='')}")['object']
|
||||
if ref['type'] == 'tag':
|
||||
ref = fetch.json(f"https://api.github.com/repos/{watch['github']}/git/tags/{ref['sha']}")['object']
|
||||
if ref['type'] != 'commit' or not re.fullmatch(r"[0-9a-f]{40}", ref['sha']):
|
||||
raise ValueError("release tag does not resolve to a commit")
|
||||
values['commit'] = ref['sha']
|
||||
variables = {k: template.format_map(values) for k, template in watch.get('variables', {}).items()}
|
||||
for name, path in watch.get('submodules', {}).items():
|
||||
entry = fetch.json(f"https://api.github.com/repos/{watch['github']}/contents/{quote(path, safe='/')}?ref={quote(values['tag'], safe='')}")
|
||||
if not entry.get('submodule_git_url') or not re.fullmatch(r"[0-9a-f]{40}", entry.get('sha', '')):
|
||||
raise ValueError(f"release does not contain submodule {path}")
|
||||
variables[name] = entry['sha']
|
||||
if any(not SCALAR.fullmatch(value) for value in variables.values()):
|
||||
raise ValueError("unsafe release variable value")
|
||||
release['variables'] = variables
|
||||
return release
|
||||
|
||||
|
||||
def sync(package, fetch, min_age=0, check=False):
|
||||
metadata = json.loads((package / ".omarchy/package.json").read_text())
|
||||
if metadata.get("sync") is False:
|
||||
return {"status": "skipped", "reason": "upstream updates held by sync=false"}
|
||||
watch = metadata["upstream"]["watch"]
|
||||
validate(watch)
|
||||
path = package / "PKGBUILD"
|
||||
original = path.read_text()
|
||||
before = read_recipe(path)
|
||||
bypass = os.environ.get("BYPASS_MIN_RELEASE_AGE") == "1"
|
||||
release = select_release(discover(watch, fetch), min_age, bypass=bypass)
|
||||
if release is None:
|
||||
return {"status": "skipped", "reason": "minimum release age"}
|
||||
current = scalar(before, "pkgver")
|
||||
if watch.get('sequence'):
|
||||
prefix, counter, identity = current.rsplit('.', 2)
|
||||
new_prefix, new_identity = release['values']['version'], release['values']['hash']
|
||||
if new_prefix == prefix:
|
||||
release['pkgver'] = current if new_identity == identity else f"{prefix}.{int(counter) + 1}.{new_identity}"
|
||||
order = vercmp(release["pkgver"], current)
|
||||
if order < 0:
|
||||
return {"status": "skipped", "current": current, "available": release["pkgver"], "reason": "upstream is older"}
|
||||
if order == 0 and not watch.get("revision_variable"):
|
||||
return {"status": "skipped", "current": current, "reason": "already current"}
|
||||
release = resolve_release_fields(watch, release, fetch)
|
||||
changed_variables = {k: v for k, v in release["variables"].items() if scalar(before, k) != v}
|
||||
if order == 0 and not changed_variables:
|
||||
return {"status": "skipped", "current": current, "reason": "already current"}
|
||||
if order == 0 and changed_variables:
|
||||
# Only a declared, forward-moving release revision can rebuild the same
|
||||
# version. A changed hash/commit alone is an immutable-release violation.
|
||||
revision_field = watch.get("revision_variable")
|
||||
if revision_field not in changed_variables or vercmp(changed_variables[revision_field], scalar(before, revision_field, "0")) <= 0:
|
||||
raise ValueError("release metadata changed without a newer version/revision")
|
||||
new_pkgrel = "1" if order > 0 else bump_pkgrel(scalar(before, "pkgrel"))
|
||||
text = replace_scalar(original, "pkgver", release["pkgver"])
|
||||
text = replace_scalar(text, "pkgrel", new_pkgrel)
|
||||
for name, value in release["variables"].items():
|
||||
text = replace_scalar(text, name, value)
|
||||
if check:
|
||||
return {"status": "available", "current": current, "release": release}
|
||||
scratch = path.with_name("PKGBUILD.sync-upstream")
|
||||
try:
|
||||
scratch.write_text(text)
|
||||
after = read_recipe(scratch)
|
||||
if scalar(after, "pkgver") != release["pkgver"] or scalar(after, "pkgrel") != new_pkgrel:
|
||||
raise ValueError("recipe did not retain the release version")
|
||||
if vercmp(complete_version(after), complete_version(before)) <= 0:
|
||||
raise ValueError("complete package version must increase")
|
||||
if before["arch"] != after["arch"]:
|
||||
raise ValueError("release changed supported architectures")
|
||||
arrays = updated_checksums(before, after, package, fetch, release, watch)
|
||||
for name, values in arrays.items():
|
||||
text = replace_array(text, name, values)
|
||||
scratch.write_text(text)
|
||||
subprocess.run(["bash", "-n", str(scratch)], check=True)
|
||||
for arch in before["arch"]:
|
||||
result = read_recipe(scratch, "x86_64" if arch == "any" else arch)
|
||||
if complete_version(result) != complete_version(after):
|
||||
raise ValueError(f"{arch}: inconsistent release version")
|
||||
for name, values in arrays.items():
|
||||
if result.get(name) != values:
|
||||
raise ValueError(f"{arch}: rewritten {name} differs from the checked source hashes")
|
||||
for name in after:
|
||||
if name == "source" or name.startswith("source_"):
|
||||
if result.get(name) != after[name]:
|
||||
raise ValueError(f"{arch}: conditional {name} differs from the checked sources; use source_<arch> arrays")
|
||||
scratch.chmod(path.stat().st_mode)
|
||||
scratch.replace(path)
|
||||
return {"status": "updated", "before": complete_version(before), "after": complete_version(after)}
|
||||
finally:
|
||||
scratch.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("command", choices=["sync", "check", "validate"])
|
||||
parser.add_argument("package", type=Path)
|
||||
parser.add_argument("--min-age", type=int, default=0)
|
||||
args = parser.parse_args()
|
||||
package = args.package.resolve()
|
||||
if args.command == "validate":
|
||||
validate(json.loads((package / ".omarchy/package.json").read_text())["upstream"]["watch"])
|
||||
return
|
||||
with tempfile.TemporaryDirectory(prefix="upstream-watch-") as cache:
|
||||
fetch = Fetcher(os.environ.get("UPSTREAM_CACHE_DIR", cache))
|
||||
print(json.dumps(sync(package, fetch, args.min_age, check=args.command == "check")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except (ValueError, KeyError, TypeError, IndexError, re.error, OSError, subprocess.CalledProcessError) as error:
|
||||
print(f"upstream watch failed: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -1,5 +1,19 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "a0f4262f94eb8a5b604146e71d42a3bb522feedf"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"debian": "https://downloads.1password.com/linux/debian/amd64/dists/beta/main/binary-amd64/Packages",
|
||||
"package": "1password",
|
||||
"pattern": "(?P<version>[0-9]+(?:\\.[0-9]+)*)~(?P<build>[0-9]+)\\.BETA",
|
||||
"version": "{version}_{build}.BETA",
|
||||
"variables": {
|
||||
"_tarver": "{version}-{build}.BETA"
|
||||
}
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "1password-beta",
|
||||
"commit": "18d2de51dc01c9a58c1e8e96262f7afadcbf0648"
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Keep the AUR x86_64 checksums and add aarch64 sources. The aarch64
|
||||
# artifacts are signed by 1Password's validpgpkeys, so we skip checksums there
|
||||
# instead of baking version-specific hashes into Omarchy metadata.
|
||||
set +u
|
||||
CARCH=x86_64 source PKGBUILD
|
||||
set -u
|
||||
|
||||
if declare -p sha256sums >/dev/null 2>&1 && [[ ${#sha256sums[@]} -ge 2 ]]; then
|
||||
x86_sums=("${sha256sums[@]}")
|
||||
elif declare -p sha256sums_x86_64 >/dev/null 2>&1 && [[ ${#sha256sums_x86_64[@]} -ge 2 ]]; then
|
||||
x86_sums=("${sha256sums_x86_64[@]}")
|
||||
else
|
||||
echo "Unable to read x86_64 checksums from PKGBUILD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sha256_from_url() {
|
||||
curl -fsSL "$1" | sha256sum | awk '{ print $1 }'
|
||||
}
|
||||
|
||||
arm_url="https://downloads.1password.com/linux/tar/beta/aarch64/1password-${_tarver}.arm64.tar.gz"
|
||||
arm_tar_sum=$(sha256_from_url "$arm_url")
|
||||
arm_sig_sum=$(sha256_from_url "$arm_url.sig")
|
||||
|
||||
emit_archdir() {
|
||||
cat <<'EOF'
|
||||
case "${CARCH}" in
|
||||
x86_64)
|
||||
_archdir="x64"
|
||||
;;
|
||||
aarch64)
|
||||
_archdir="arm64"
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
}
|
||||
|
||||
emit_sources() {
|
||||
cat <<EOF
|
||||
source=()
|
||||
sha256sums=()
|
||||
source_x86_64=(https://downloads.1password.com/linux/tar/beta/x86_64/1password-\${_tarver}.x64.tar.gz{,.sig})
|
||||
source_aarch64=(https://downloads.1password.com/linux/tar/beta/aarch64/1password-\${_tarver}.arm64.tar.gz{,.sig})
|
||||
sha256sums_x86_64=('${x86_sums[0]}'
|
||||
'${x86_sums[1]}')
|
||||
sha256sums_aarch64=('$arm_tar_sum'
|
||||
'$arm_sig_sum')
|
||||
EOF
|
||||
}
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
skip_checksums=false
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$skip_checksums" == true ]]; then
|
||||
[[ "$line" == ")" ]] && skip_checksums=false
|
||||
continue
|
||||
fi
|
||||
|
||||
case "$line" in
|
||||
'_tar="1password-${_tarver}.x64.tar.gz"')
|
||||
emit_archdir >> "$tmpfile"
|
||||
;;
|
||||
"arch=('x86_64')")
|
||||
echo "arch=('x86_64' 'aarch64')" >> "$tmpfile"
|
||||
;;
|
||||
source=\(*)
|
||||
emit_sources >> "$tmpfile"
|
||||
;;
|
||||
sha256sums=\(*)
|
||||
[[ "$line" == *")" ]] || skip_checksums=true
|
||||
;;
|
||||
*)
|
||||
line=${line//1password-\$\{_tarver\}.x64/1password-\$\{_tarver\}.\$\{_archdir\}}
|
||||
printf '%s\n' "$line" >> "$tmpfile"
|
||||
;;
|
||||
esac
|
||||
done < PKGBUILD
|
||||
|
||||
mv "$tmpfile" PKGBUILD
|
||||
@@ -1,6 +1,6 @@
|
||||
pkgname=1password-beta
|
||||
|
||||
_tarver=8.12.34-29.BETA
|
||||
_tarver=8.12.40-27.BETA
|
||||
case "${CARCH}" in
|
||||
x86_64)
|
||||
_archdir="x64"
|
||||
@@ -9,8 +9,8 @@ case "${CARCH}" in
|
||||
_archdir="arm64"
|
||||
;;
|
||||
esac
|
||||
pkgver=${_tarver//-/_}
|
||||
pkgrel=29.1
|
||||
pkgver=8.12.40_27.BETA
|
||||
pkgrel=2
|
||||
conflicts=('1password' '1password-beta-bin')
|
||||
pkgdesc="Password manager and secure wallet"
|
||||
arch=('x86_64' 'aarch64')
|
||||
@@ -22,10 +22,8 @@ source=()
|
||||
sha256sums=()
|
||||
source_x86_64=(https://downloads.1password.com/linux/tar/beta/x86_64/1password-${_tarver}.x64.tar.gz{,.sig})
|
||||
source_aarch64=(https://downloads.1password.com/linux/tar/beta/aarch64/1password-${_tarver}.arm64.tar.gz{,.sig})
|
||||
sha256sums_x86_64=('6894b283a534cf94b07903fb38966a0aab2e37f75ee3848ce340308819aadddb'
|
||||
'8d4df4d0a80d2be7aad7d91ad964a750c8f32db5007261045003c191690c8246')
|
||||
sha256sums_aarch64=('c77ce6ddf36dbd6054c64d91b7f644274d3218f465fd60e211d0296f6443124a'
|
||||
'91c7249a1cf5e7924ef2810cb0cb1b893d8a9a424b373b433f45d7aaa5a8369b')
|
||||
sha256sums_x86_64=('1327d102255b5c347e6c5e19b1a6581986446e06848015b93b5d7b10b9bc3f52' '2a19fba2b81ade500d9707a20829930d4fca972254fdc653ffa27cee57638098')
|
||||
sha256sums_aarch64=('54b14cae2a676480f3f2df7b85e42bf389d1f84207b0a4ad1e32382071625146' '3a55dc9cdee5729e4bdf6830e04d9813c11546b328003800a837df83f79e33f9')
|
||||
validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22')
|
||||
|
||||
package() {
|
||||
@@ -42,7 +40,7 @@ package() {
|
||||
"${pkgdir}/usr/share/icons/hicolor/${resolution}/apps/1password.png"
|
||||
done
|
||||
# Install desktop file
|
||||
install -Dm0644 resources/1password.desktop -t "${pkgdir}"/usr/share/applications/
|
||||
install -Dm0644 resources/com.onepassword.OnePassword.desktop -t "${pkgdir}"/usr/share/applications/
|
||||
|
||||
# Fill in policy kit file with a list of (the first 10) human users of the system.
|
||||
export POLICY_OWNERS
|
||||
@@ -65,8 +63,7 @@ EOF" > ./com.1password.1Password.policy
|
||||
# Cleanup un-needed files
|
||||
rm "${pkgdir}"/opt/1Password/com.1password.1Password.policy "${pkgdir}"/opt/1Password/com.1password.1Password.policy.tpl "${pkgdir}"/opt/1Password/install_biometrics_policy.sh
|
||||
rm -r "${pkgdir}"/opt/1Password/resources/icons/
|
||||
rm "${pkgdir}"/opt/1Password/resources/1password.desktop "${pkgdir}"/opt/1Password/resources/custom_allowed_browsers
|
||||
|
||||
rm "${pkgdir}"/opt/1Password/resources/com.onepassword.OnePassword.desktop "${pkgdir}"/opt/1Password/resources/custom_allowed_browsers
|
||||
# Symlink /usr/bin executable to opt
|
||||
install -dm0755 "${pkgdir}"/usr/bin
|
||||
ln -s /opt/1Password/1password "${pkgdir}"/usr/bin/1password
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "b0d208821677a5dbb883a8b92f06a5c92b9e861a"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"json": "https://app-updates.agilebits.com/check/1/0/CLI2/en/0",
|
||||
"path": "version"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "1password-cli",
|
||||
"commit": "b0d208821677a5dbb883a8b92f06a5c92b9e861a"
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
# Contributors: Felix Seidel, Claudia Pellegrino, Liu Yuxuan
|
||||
|
||||
pkgname=1password-cli
|
||||
pkgver=2.39.0
|
||||
pkgrel=1
|
||||
pkgver=2.40.0
|
||||
pkgrel=2
|
||||
pkgdesc="1Password command line tool"
|
||||
arch=('x86_64' 'i686' 'arm' 'armv6h' 'aarch64')
|
||||
url="https://app-updates.agilebits.com/product_history/CLI2"
|
||||
@@ -18,11 +18,11 @@ source_arm=("https://cache.agilebits.com/dist/1P/op2/pkg/v${pkgver}/op_linux_arm
|
||||
source_armv6h=("${source_arm}")
|
||||
source_aarch64=("https://cache.agilebits.com/dist/1P/op2/pkg/v${pkgver}/op_linux_arm64_v${pkgver}.zip")
|
||||
|
||||
sha256sums_x86_64=('6fba7f376b6c6dec49f41b06408930a43ad064cce103c6a2ce5b3d0413a86434')
|
||||
sha256sums_i686=('387d95f046ec9334e9de63514f96767fdd5dacbae292eb086fdfa0b0da310ec4')
|
||||
sha256sums_arm=('673913f24ff57ce43e9d25ee451121d4733d188f45f6ab0468983fad85f8cc42')
|
||||
sha256sums_armv6h=("${sha256sums_arm}")
|
||||
sha256sums_aarch64=('829baeff1c07e055cfa132031b1d9f2282ccdf5076258e482caf2fda70aea5d0')
|
||||
sha256sums_x86_64=('74277219e8da60958c00f9aee9d2023225e98fdda8bfd2156a5d9e85e0edaab3')
|
||||
sha256sums_i686=('adb1da45c0a40bb97c1e1ad62119d0a1aeb4ed9d979c06a7b1f95a48d9142b3d')
|
||||
sha256sums_arm=('cb9a2e938b542eac668e847bf555293549ad6ecc014216c99a66f1348728e354')
|
||||
sha256sums_armv6h=('cb9a2e938b542eac668e847bf555293549ad6ecc014216c99a66f1348728e354')
|
||||
sha256sums_aarch64=('0e8ac99ee93d661aa725dc24a5ef8bf344741d224064a5dfb469dc689faec86a')
|
||||
|
||||
check() {
|
||||
if (( ! SKIPPGPCHECK )); then
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "e323d0d1f8dea6b75bb651ce14acc73904cd0326"
|
||||
"upstream": {
|
||||
"debian": "https://downloads.1password.com/linux/debian/amd64/dists/stable/main/binary-amd64/Packages",
|
||||
"package": "1password",
|
||||
"sources": {
|
||||
"x86_64": [
|
||||
"https://downloads.1password.com/linux/tar/stable/x86_64/1password-{pkgver}.x64.tar.gz",
|
||||
"https://downloads.1password.com/linux/tar/stable/x86_64/1password-{pkgver}.x64.tar.gz.sig"
|
||||
],
|
||||
"aarch64": [
|
||||
"https://downloads.1password.com/linux/tar/stable/aarch64/1password-{pkgver}.arm64.tar.gz",
|
||||
"https://downloads.1password.com/linux/tar/stable/aarch64/1password-{pkgver}.arm64.tar.gz.sig"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,47 @@
|
||||
pkgname=1password
|
||||
|
||||
_tarver=8.12.34
|
||||
_tar="1password-${_tarver}.x64.tar.gz"
|
||||
pkgver=${_tarver//-/_}
|
||||
pkgrel=34
|
||||
pkgver=8.12.40
|
||||
pkgrel=2
|
||||
conflicts=('1password-beta' '1password-beta-bin')
|
||||
pkgdesc="Password manager and secure wallet"
|
||||
arch=('x86_64')
|
||||
arch=('x86_64' 'aarch64')
|
||||
url='https://1password.com'
|
||||
license=('LicenseRef-1Password-Proprietary')
|
||||
options=(!strip)
|
||||
install="1password.install"
|
||||
source=(https://downloads.1password.com/linux/tar/stable/${CARCH}/${_tar}{,.sig})
|
||||
sha256sums=('297784aa66770b645607a7f04c9ba2c4aebed4f46d21202487f521ba572b7b13'
|
||||
'ec085bef60de748895d3c51a8208301ba2ac8fb47db99334539ba4bd1d3260d7'
|
||||
source_x86_64=(
|
||||
"https://downloads.1password.com/linux/tar/stable/x86_64/1password-${pkgver}.x64.tar.gz"
|
||||
"https://downloads.1password.com/linux/tar/stable/x86_64/1password-${pkgver}.x64.tar.gz.sig"
|
||||
)
|
||||
source_aarch64=(
|
||||
"https://downloads.1password.com/linux/tar/stable/aarch64/1password-${pkgver}.arm64.tar.gz"
|
||||
"https://downloads.1password.com/linux/tar/stable/aarch64/1password-${pkgver}.arm64.tar.gz.sig"
|
||||
)
|
||||
sha256sums_x86_64=(
|
||||
'0ae9645d31be78a8fb57d15f49e5fa4f0b67a0b3608797a410e8fd36f0206266'
|
||||
'2f777820dc2eb6e7b7b38f4557a897f97fb3259443261fdfb2008127bd975817'
|
||||
)
|
||||
sha256sums_aarch64=(
|
||||
'7476c0fc8215338cd9f304b14822688010fd8ed54d5ea4367c0bbbf72845be1e'
|
||||
'80412176560a3f76180f7c05ae7ebafe21fac764349cfeed71fe498ee25564ee'
|
||||
)
|
||||
validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22')
|
||||
|
||||
package() {
|
||||
depends=('hicolor-icon-theme' 'libgtk-3.so=0' 'nss' 'xdg-utils')
|
||||
|
||||
# 1Password names its tarballs by a vendor architecture suffix. arch=()
|
||||
# above already limits which architectures makepkg builds, so no fallback
|
||||
# branch is needed here; keeping this inside package() means sourcing the
|
||||
# PKGBUILD without CARCH (as the version check does) still reads the
|
||||
# version correctly.
|
||||
local _tararch
|
||||
case "$CARCH" in
|
||||
x86_64) _tararch=x64 ;;
|
||||
aarch64) _tararch=arm64 ;;
|
||||
esac
|
||||
|
||||
# Go to source directory
|
||||
cd "1password-${_tarver}.x64"
|
||||
cd "1password-${pkgver}.${_tararch}"
|
||||
|
||||
# Install icons
|
||||
resolutions=(32x32 64x64 256x256 512x512)
|
||||
@@ -31,7 +51,14 @@ package() {
|
||||
"${pkgdir}/usr/share/icons/hicolor/${resolution}/apps/1password.png"
|
||||
done
|
||||
# Install desktop file
|
||||
install -Dm0644 resources/1password.desktop -t "${pkgdir}"/usr/share/applications/
|
||||
install -Dm0644 resources/com.onepassword.OnePassword.desktop \
|
||||
"${pkgdir}/usr/share/applications/1password.desktop"
|
||||
|
||||
# 1Password reads the display scale itself, the way Electron apps do, and
|
||||
# comes up oversized next to every other window on a scaled monitor. Pin it
|
||||
# and let the compositor do the scaling.
|
||||
sed -i 's|^Exec=.*|Exec=/opt/1Password/1password --force-device-scale-factor=1 %U|' \
|
||||
"${pkgdir}/usr/share/applications/1password.desktop"
|
||||
|
||||
# Fill in policy kit file with a list of (the first 10) human users of the system.
|
||||
export POLICY_OWNERS
|
||||
@@ -49,12 +76,12 @@ EOF" > ./com.1password.1Password.policy
|
||||
# Move package contents to /opt/1Password
|
||||
cd "${srcdir}"
|
||||
install -dm0755 "${pkgdir}"/opt
|
||||
mv "1password-${_tarver}.x64" "${pkgdir}/opt/1Password"
|
||||
mv "1password-${pkgver}.${_tararch}" "${pkgdir}/opt/1Password"
|
||||
|
||||
# Cleanup un-needed files
|
||||
rm "${pkgdir}"/opt/1Password/com.1password.1Password.policy "${pkgdir}"/opt/1Password/com.1password.1Password.policy.tpl "${pkgdir}"/opt/1Password/install_biometrics_policy.sh
|
||||
rm -r "${pkgdir}"/opt/1Password/resources/icons/
|
||||
rm "${pkgdir}"/opt/1Password/resources/1password.desktop "${pkgdir}"/opt/1Password/resources/custom_allowed_browsers
|
||||
rm "${pkgdir}"/opt/1Password/resources/com.onepassword.OnePassword.desktop "${pkgdir}"/opt/1Password/resources/custom_allowed_browsers
|
||||
|
||||
# Symlink /usr/bin executable to opt
|
||||
install -dm0755 "${pkgdir}"/usr/bin
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "04592d5713f9c09ea2d81cf4b8476714d80014bc"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "omacom/aether",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "aether",
|
||||
"commit": "0f047f40a200121075ca3cedce59ddf226f2a0f1"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
# Maintainer: Bjarne Øverli <bjarne@oever.li>
|
||||
pkgname=aether
|
||||
pkgver=4.29.6
|
||||
pkgrel=1
|
||||
pkgver=4.32.0
|
||||
pkgrel=2
|
||||
pkgdesc='Desktop theming application - extract colors from wallpapers and apply cohesive themes'
|
||||
arch=('x86_64' 'aarch64')
|
||||
url='https://github.com/omacom-io/aether'
|
||||
url='https://github.com/omacom/aether'
|
||||
license=('MIT')
|
||||
depends=('webkit2gtk-4.1' 'gtk3')
|
||||
source=("aether-${pkgver}.tar.gz::https://github.com/omacom-io/aether/archive/refs/tags/v${pkgver}.tar.gz")
|
||||
source_x86_64=("aether-linux-amd64-${pkgver}::https://github.com/omacom-io/aether/releases/download/v${pkgver}/aether-linux-amd64")
|
||||
source_aarch64=("aether-linux-arm64-${pkgver}::https://github.com/omacom-io/aether/releases/download/v${pkgver}/aether-linux-arm64")
|
||||
sha256sums=('8b2e97283007c9eefda879e17c5fecb9d59fc90bf2160af93eeba57f11fcdb25')
|
||||
sha256sums_x86_64=('47b5afa4144b3a3cd7755524956ffe6b074ed5f2f90dadda23e424efeaf88790')
|
||||
sha256sums_aarch64=('52d1ffb201970ddb6205882a8e1c583cae63470d4180f4bc2fc6298dfb71ddd9')
|
||||
source=("aether-${pkgver}.tar.gz::https://github.com/omacom/aether/archive/refs/tags/v${pkgver}.tar.gz")
|
||||
source_x86_64=("aether-linux-amd64-${pkgver}::https://github.com/omacom/aether/releases/download/v${pkgver}/aether-linux-amd64")
|
||||
source_aarch64=("aether-linux-arm64-${pkgver}::https://github.com/omacom/aether/releases/download/v${pkgver}/aether-linux-arm64")
|
||||
sha256sums=('3234e5138a46699f8f47330ba582244d5063c8356ffaae1458323e4dda02be82')
|
||||
sha256sums_x86_64=('c705a4abef734b17ae37cdbff58a9a796517e6aa5f93ef18b68e3ee99d547ff9')
|
||||
sha256sums_aarch64=('f70195deba008204d58661c3ec1442a16e63f3358b4579969facb0e55dbf63c4')
|
||||
noextract=("aether-linux-amd64-${pkgver}" "aether-linux-arm64-${pkgver}")
|
||||
|
||||
package() {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "local",
|
||||
"channels": [
|
||||
"edge"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
From 375e80ec3826b54618fdd5f2db708c0f2bb936ae Mon Sep 17 00:00:00 2001
|
||||
From: Marcelo Alcantara <maralc@gmail.com>
|
||||
Date: Wed, 16 Sep 2026 00:39:04 +1000
|
||||
Subject: [PATCH] drm: re-read possible CRTCs when rescanning connectors
|
||||
|
||||
A driver can change a connector's possible CRTCs at runtime. Apple's DCP
|
||||
driver narrows a Type-C port's encoder to the display pipeline the fabric
|
||||
routed it to and relies on the following hotplug for userspace to re-read
|
||||
it. possibleCrtcs was only read when the connector was first created, so a
|
||||
rerouted port kept a stale mask and was never assigned its now-free CRTC
|
||||
until the compositor restarted.
|
||||
---
|
||||
src/backend/drm/DRM.cpp | 7 +++++++
|
||||
1 file changed, 7 insertions(+)
|
||||
|
||||
diff --git a/src/backend/drm/DRM.cpp b/src/backend/drm/DRM.cpp
|
||||
index 6618a26..b492dac 100644
|
||||
--- a/src/backend/drm/DRM.cpp
|
||||
+++ b/src/backend/drm/DRM.cpp
|
||||
@@ -1287,6 +1287,13 @@ void Aquamarine::CDRMBackend::scanConnectors() {
|
||||
} else {
|
||||
backend->log(AQ_LOG_DEBUG, std::format("drm: Connector id {} already initialized", connectorID));
|
||||
conn = *it;
|
||||
+
|
||||
+ // drivers may narrow or widen these on hotplug, e.g. when a Type-C port is rerouted to another pipeline
|
||||
+ const auto possibleCrtcs = drmModeConnectorGetPossibleCrtcs(gpu->fd, drmConn);
|
||||
+ if (possibleCrtcs && possibleCrtcs != conn->possibleCrtcs) {
|
||||
+ backend->log(AQ_LOG_DEBUG, std::format("drm: Connector {} possible CRTCs changed {:#x} -> {:#x}", conn->szName, conn->possibleCrtcs, possibleCrtcs));
|
||||
+ conn->possibleCrtcs = possibleCrtcs;
|
||||
+ }
|
||||
}
|
||||
|
||||
conn->status = drmConn->connection;
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Maintainer: Caleb Maclennan <caleb@alerque.com>
|
||||
# Contributor: Aaron Blasko <blaskoazzolaaaron [at] gmail.com>
|
||||
#
|
||||
# Arch's aquamarine 0.15.1-1 plus one patch, carried on edge for Apple Silicon:
|
||||
# re-read a connector's possible CRTCs when rescanning, so a Type-C port the
|
||||
# DCP fabric reroutes to another display pipeline gets a CRTC without
|
||||
# restarting the compositor. Drop the carry once hyprwm releases the fix.
|
||||
|
||||
pkgname=aquamarine
|
||||
pkgver=0.15.1
|
||||
pkgrel=1.1
|
||||
pkgdesc='a very light linux rendering backend library'
|
||||
arch=(aarch64)
|
||||
url="https://github.com/hyprwm/$pkgname"
|
||||
license=(BSD-3-Clause)
|
||||
depends=(libgcc
|
||||
libstdc++
|
||||
glibc # libc.so libm.so
|
||||
hyprutils libhyprutils.so
|
||||
libdisplay-info libdisplay-info.so
|
||||
libdrm # libdrm.so
|
||||
libglvnd libEGL.so
|
||||
libinput # libinput.so
|
||||
mesa # libgbm.so
|
||||
opengl-driver
|
||||
pixman
|
||||
seatd libseat.so
|
||||
systemd-libs libudev.so
|
||||
wayland libwayland-client.so
|
||||
wayland-protocols)
|
||||
makedepends=(cmake
|
||||
hyprwayland-scanner)
|
||||
provides=("lib$pkgname.so")
|
||||
_archive="$pkgname-$pkgver"
|
||||
source=("$url/archive/v$pkgver/$_archive.tar.gz"
|
||||
0001-drm-re-read-possible-CRTCs-when-rescanning-connectors.patch)
|
||||
sha256sums=('2f9de98c0bd1b7b1b09c576e390a2fef436449762fb334163c414f0c300296f2'
|
||||
'50e9e38ff915b18074dc9b5dd1d07d7f24e340e99f254476d6abe578eece7864')
|
||||
|
||||
prepare() {
|
||||
cd "$_archive"
|
||||
patch -Np1 -i ../0001-drm-re-read-possible-CRTCs-when-rescanning-connectors.patch
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "$_archive"
|
||||
# cc1plus needs more than 8 MiB of stack for DRM.cpp. GCC raises its own
|
||||
# limit natively, but under QEMU user-mode emulation (the aarch64 builder)
|
||||
# the guest stack is fixed at exec and setrlimit is ignored, so the compiler
|
||||
# segfaults. QEMU reads this at exec; native builds ignore it. The object
|
||||
# code is identical either way.
|
||||
export QEMU_STACK_SIZE=64M
|
||||
cmake -B build \
|
||||
-D CMAKE_INSTALL_PREFIX=/usr \
|
||||
-D CMAKE_BUILD_TYPE=Release
|
||||
cmake --build build
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$_archive"
|
||||
DESTDIR="$pkgdir" cmake --install build
|
||||
install -Dm0644 -t "$pkgdir/usr/share/licenses/$pkgname" LICENSE
|
||||
}
|
||||
@@ -4,7 +4,7 @@ epoch=1
|
||||
pkgver=0.6.0
|
||||
pkgrel=2
|
||||
pkgdesc="Control brightness on Apple Displays connected via USB-C"
|
||||
arch=('x86_64')
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://github.com/omakasui/asdcontrol"
|
||||
license=('GPL2')
|
||||
options=('!debug')
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[asusctl]
|
||||
source = "git"
|
||||
git = "https://gitlab.com/asus-linux/asusctl.git"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"upstream_commit": "ed336f7dbd5e8bee333aeaa6246da89241f1c8e6"
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
diff --git a/PKGBUILD b/PKGBUILD
|
||||
index 55d0c22..8897aec 100644
|
||||
--- a/PKGBUILD
|
||||
+++ b/PKGBUILD
|
||||
@@ -6,7 +6,7 @@ pkgname=(
|
||||
asusctl
|
||||
rog-control-center
|
||||
)
|
||||
-pkgver=6.3.8
|
||||
+pkgver=6.4.0
|
||||
pkgrel=1
|
||||
pkgdesc="A control daemon, tools, and a collection of crates for interacting with ASUS ROG laptops"
|
||||
arch=('x86_64')
|
||||
@@ -28,2 +28,2 @@
|
||||
-source=("git+https://gitlab.com/asus-linux/asusctl.git#tag=$pkgver")
|
||||
-b2sums=('01269bc9fe63c1ce37568f9085c25dbcaaaa5cd9f51339d4c6904600d179ec826dace289d6e4e3501988fce5800da98e9cfa18b21c40da152d91c4f7fe821ffe')
|
||||
+source=("git+https://github.com/opengamingcollective/asusctl.git#tag=$pkgver")
|
||||
+b2sums=('0cfdc7f792fc6499cde9e4c9c6d82e484605e1b2878bb9f3a0e0696f5910b9ba7898f124fecd5fb3f39a0787f81e5163721606290099d757365dc3d6715a1bcf')
|
||||
@@ -1,108 +0,0 @@
|
||||
# Maintainer: Fabian Bornschein <fabiscafe@archlinux.org>
|
||||
# Contributor: Static_Rocket
|
||||
|
||||
pkgbase=asusctl
|
||||
pkgname=(
|
||||
asusctl
|
||||
rog-control-center
|
||||
)
|
||||
pkgver=6.4.0
|
||||
pkgrel=1.1
|
||||
pkgdesc="A control daemon, tools, and a collection of crates for interacting with ASUS ROG laptops"
|
||||
arch=('x86_64')
|
||||
url="https://asus-linux.org"
|
||||
license=('MPL-2.0')
|
||||
makedepends=(
|
||||
clang
|
||||
cmake
|
||||
fontconfig
|
||||
git
|
||||
hicolor-icon-theme
|
||||
libayatana-appindicator
|
||||
libinput
|
||||
libusb
|
||||
rust
|
||||
seatd
|
||||
systemd
|
||||
)
|
||||
source=("git+https://github.com/opengamingcollective/asusctl.git#tag=$pkgver")
|
||||
b2sums=('0cfdc7f792fc6499cde9e4c9c6d82e484605e1b2878bb9f3a0e0696f5910b9ba7898f124fecd5fb3f39a0787f81e5163721606290099d757365dc3d6715a1bcf')
|
||||
|
||||
prepare() {
|
||||
cd "${pkgbase}"
|
||||
|
||||
# Keep rust/cargo build-dependency management inside the build directory
|
||||
export CARGO_HOME="${srcdir}/cargo"
|
||||
|
||||
# Follow Rust package guidelines
|
||||
## https://wiki.archlinux.org/title/Rust_package_guidelines
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')"
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "${pkgbase}"
|
||||
|
||||
# Keep rust/cargo build-dependency management inside the build directory
|
||||
export CARGO_HOME="${srcdir}/cargo"
|
||||
|
||||
# Follow Rust package guidelines
|
||||
## https://wiki.archlinux.org/title/Rust_package_guidelines
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
export CARGO_TARGET_DIR=target
|
||||
|
||||
make build
|
||||
}
|
||||
|
||||
package_asusctl() {
|
||||
pkgdesc="${pkgdesc/tools/CLI tools}"
|
||||
depends=(
|
||||
glibc
|
||||
libgcc
|
||||
libusb
|
||||
systemd
|
||||
systemd-libs
|
||||
)
|
||||
conflicts=(gnome-shell-extension-asusctl-gnome)
|
||||
install=asusctl.install
|
||||
optdepends=(
|
||||
'acpi_call: fan control'
|
||||
'asusctltray: tray profile switcher'
|
||||
'rog-control-center: app to control asusctl'
|
||||
'supergfxctl: hybrid GPU control'
|
||||
)
|
||||
|
||||
cd "${pkgbase}"
|
||||
export CARGO_HOME="${srcdir}/cargo"
|
||||
make DESTDIR="${pkgdir}" \
|
||||
install-asusctl \
|
||||
install-asusd \
|
||||
install-asusd_user \
|
||||
install-asus-shutdown \
|
||||
install-data-asusd \
|
||||
install-data-asusd_user
|
||||
}
|
||||
|
||||
package_rog-control-center() {
|
||||
depends=(
|
||||
asusctl
|
||||
fontconfig
|
||||
freetype2
|
||||
glibc
|
||||
hicolor-icon-theme
|
||||
libayatana-appindicator
|
||||
libgcc
|
||||
libinput
|
||||
libxkbcommon
|
||||
mesa
|
||||
seatd
|
||||
systemd-libs
|
||||
)
|
||||
pkgdesc="App to control asusctl"
|
||||
|
||||
cd "${pkgbase}"
|
||||
export CARGO_HOME="${srcdir}/cargo"
|
||||
make DESTDIR="${pkgdir}" \
|
||||
install-data-rog_gui \
|
||||
install-rog_gui
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
post_install() {
|
||||
printf ":: asusd provides a service that is activated by an udev rule on\n"
|
||||
printf ":: startup. Please reboot the system or run\n"
|
||||
printf ":: # systemctl start asusd.service\n"
|
||||
printf ":: to make it work.\n"
|
||||
printf ":: See https://gitlab.com/asus-linux/asusctl#kernel-support.\n"
|
||||
printf ":: for latest required kernel patches/versions\n"
|
||||
}
|
||||
|
||||
post_upgrade() {
|
||||
if systemctl is-active asusd.service --quiet
|
||||
then
|
||||
printf ":: asusd service will be restarted…\n"
|
||||
systemctl daemon-reload
|
||||
systemctl restart asusd.service
|
||||
fi
|
||||
printf ":: See https://gitlab.com/asus-linux/asusctl#kernel-support.\n"
|
||||
printf ":: for latest required kernel patches/versions\n"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"source": "local",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)",
|
||||
"git_tags": "https://github.com/AsahiLinux/avd-fw.git"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
# Open replacement firmware for the Apple Video Decoder.
|
||||
# Built from AsahiLinux sources, without extracting proprietary firmware.
|
||||
|
||||
pkgname=avd-fw
|
||||
pkgver=0.1
|
||||
pkgrel=1
|
||||
pkgdesc='Open replacement firmware for the Apple Video Decoder (AVD) on Apple Silicon'
|
||||
# This recipe uses the native ARM linker and serves Apple Silicon systems.
|
||||
# Restrict both builds and publication to aarch64.
|
||||
arch=('aarch64')
|
||||
url='https://github.com/AsahiLinux/avd-fw'
|
||||
license=('MIT')
|
||||
# clang cross-compiles to arm-none-eabi out of the box, so no arm-none-eabi
|
||||
# toolchain is required; llvm supplies llvm-objcopy, which meson.build looks
|
||||
# up by name to turn each linked ELF into a padded raw image.
|
||||
makedepends=('meson' 'clang' 'llvm')
|
||||
# !buildflags is load-bearing: makepkg's CFLAGS/LDFLAGS target the aarch64 host
|
||||
# and would be injected into a bare-metal Cortex-M3 build. !strip keeps makepkg
|
||||
# from running the host strip over raw firmware images.
|
||||
options=('!strip' '!debug' '!buildflags' '!lto')
|
||||
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
|
||||
sha256sums=('2e131244275cb15c94243e41eec8a2a665ec895cfe3a818181549db991e62c67')
|
||||
|
||||
build() {
|
||||
# The cross file selects clang, pins the host machine to cortex-m3 and sets
|
||||
# libdir=lib, which lands the images in /usr/lib/firmware/apple.
|
||||
#
|
||||
# Optimization is pinned to match upstream's Makefile (-O2). Meson's default
|
||||
# buildtype is "debug" (-O0 -g), which would ship unoptimized firmware; the
|
||||
# MMIO accessors in src/util.c go through a volatile typedef, so -O2 cannot
|
||||
# elide register reads or writes.
|
||||
meson setup \
|
||||
--cross-file "$pkgname-$pkgver/llvm.ini" \
|
||||
--prefix=/usr \
|
||||
-Doptimization=2 \
|
||||
-Ddebug=false \
|
||||
"$pkgname-$pkgver" build
|
||||
|
||||
meson compile -C build
|
||||
}
|
||||
|
||||
package() {
|
||||
meson install -C build --destdir "$pkgdir"
|
||||
|
||||
# meson installs custom_target outputs 0755; these are firmware blobs, not
|
||||
# programs, and every other firmware file on the system is 0644.
|
||||
chmod 644 "$pkgdir"/usr/lib/firmware/apple/*.bin
|
||||
|
||||
install -Dm644 "$pkgname-$pkgver/LICENSE" \
|
||||
"$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"origin": {
|
||||
"aur": "bambustudio-bin",
|
||||
"commit": "b962c12d14873f94669e0e256a444f957b1843cd"
|
||||
},
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"regex": "https://api.github.com/repos/bambulab/BambuStudio/releases/latest",
|
||||
"pattern": "\"name\"\\s*:\\s*\"BambuStudio_ubuntu24\\.04-v(?P<version>[0-9]+(?:\\.[0-9]+)*)-(?P<build>[0-9]+)\\.AppImage\"",
|
||||
"variables": {
|
||||
"_build": "{build}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[Desktop Entry]
|
||||
Name=Bambu Studio
|
||||
GenericName=3D Printing Software
|
||||
Comment=Slicer for Bambu Lab and other 3D printers
|
||||
Exec=/usr/bin/bambu-studio %U
|
||||
Icon=BambuStudio
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Graphics;3DGraphics;Engineering;
|
||||
MimeType=x-scheme-handler/bambustudio;x-scheme-handler/bambustudioopen;model/stl;model/3mf;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;application/x-amf;
|
||||
Keywords=3D;Printing;Slicer;gcode;stl;3mf;
|
||||
StartupWMClass=bambu-studio
|
||||
@@ -0,0 +1,48 @@
|
||||
# Maintainer: goll <adrian.goll+aur[at]gmail>
|
||||
# Contributor: George Woodall <georgewoodall82@gmail.com>
|
||||
pkgname=bambustudio-bin
|
||||
pkgver=02.08.02.61
|
||||
pkgrel=1
|
||||
pkgdesc="PC Software for BambuLab's 3D printers"
|
||||
arch=("x86_64")
|
||||
url="https://github.com/bambulab/BambuStudio"
|
||||
license=('AGPL-3.0-only')
|
||||
conflicts=('bambustudio' 'bambustudio-git')
|
||||
depends=('cairo' 'dbus' 'fontconfig' 'gcc-libs' 'glib2' 'glibc'
|
||||
'gst-libav' 'gst-plugins-base-libs' 'gstreamer' 'gtk3' 'libglvnd'
|
||||
'libx11' 'mesa' 'pango' 'wayland' 'webkit2gtk-4.1')
|
||||
makedepends=('7zip')
|
||||
options=('!strip' '!debug')
|
||||
# The upstream watch updates the timestamp together with pkgver.
|
||||
_build=20260820225108
|
||||
source=("bambustudio-${pkgver}.AppImage::https://github.com/bambulab/BambuStudio/releases/download/v${pkgver}/BambuStudio_ubuntu24.04-v${pkgver}-${_build}.AppImage"
|
||||
"BambuStudio.desktop"
|
||||
"bambu-studio")
|
||||
noextract=("bambustudio-${pkgver}.AppImage")
|
||||
sha256sums=(
|
||||
'd501b103fac5424513ec0e8d6bc145fb30719de2c7d94d7320d723740c81a7fd'
|
||||
'f10718a8b201cad64800746fe8167ccc032c545d05f7ad8caa99eb5fb975f2a1'
|
||||
'a3a5c8f6a8b287e42b93957e9602621923c766e0b6a3f10c14eca10b023b15f2'
|
||||
)
|
||||
|
||||
prepare() {
|
||||
# Read the embedded SquashFS without executing or modifying the AppImage.
|
||||
rm -rf "$srcdir/squashfs-root"
|
||||
7z x "$srcdir/bambustudio-${pkgver}.AppImage" -o"$srcdir/squashfs-root" >/dev/null
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$srcdir/squashfs-root"
|
||||
install -Dm755 AppRun "$pkgdir/opt/$pkgname/AppRun"
|
||||
cp -a bin resources "$pkgdir/opt/$pkgname/"
|
||||
|
||||
local icon size
|
||||
for icon in usr/share/icons/hicolor/*/apps/BambuStudio.png; do
|
||||
size="${icon#usr/share/icons/hicolor/}"
|
||||
install -Dm644 "$icon" "$pkgdir/usr/share/icons/hicolor/$size"
|
||||
done
|
||||
|
||||
install -Dm755 "$srcdir/bambu-studio" "$pkgdir/usr/bin/bambu-studio"
|
||||
install -Dm644 "$srcdir/BambuStudio.desktop" \
|
||||
"$pkgdir/usr/share/applications/BambuStudio.desktop"
|
||||
}
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
exec "/opt/bambustudio-bin/AppRun" "$@"
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "fe2a6345e271f618dc3adf31e48557a717866395"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "basecamp/basecamp-cli",
|
||||
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "basecamp-cli",
|
||||
"commit": "89c4eb5a0ac9ffe98aecd4ea8b789cf1fee42bf1"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Basecamp <support@basecamp.com>
|
||||
pkgname=basecamp-cli
|
||||
pkgver=0.9.1
|
||||
pkgver=0.12.0
|
||||
pkgrel=1
|
||||
pkgdesc="CLI for Basecamp project management"
|
||||
arch=('x86_64' 'aarch64')
|
||||
@@ -13,10 +13,10 @@ optdepends=(
|
||||
'zsh: for zsh shell completions'
|
||||
'fish: for fish shell completions'
|
||||
)
|
||||
source_x86_64=("https://github.com/basecamp/basecamp-cli/releases/download/v0.9.1/basecamp_${pkgver}_linux_amd64.tar.gz")
|
||||
source_aarch64=("https://github.com/basecamp/basecamp-cli/releases/download/v0.9.1/basecamp_${pkgver}_linux_arm64.tar.gz")
|
||||
sha256sums_x86_64=('bcb1fa3a03e702bbf81a3004ded4bc7808605e41106cbc768ba4006b89e0db2f')
|
||||
sha256sums_aarch64=('ff50313024b6ca14e40146e30e1c9c9c00a4e4103fe55615eeee3e7c359d88d1')
|
||||
source_x86_64=("https://github.com/basecamp/basecamp-cli/releases/download/v${pkgver}/basecamp_${pkgver}_linux_amd64.tar.gz")
|
||||
source_aarch64=("https://github.com/basecamp/basecamp-cli/releases/download/v${pkgver}/basecamp_${pkgver}_linux_arm64.tar.gz")
|
||||
sha256sums_x86_64=('25d61c38de5660e97855661a2bf7329264912fcd46e2b3fb893fbc70bb467b5c')
|
||||
sha256sums_aarch64=('1f692d2a8cc733ef95232eeb107414e32f1dd3228a0dde6c9039538661e0f2ca')
|
||||
|
||||
package() {
|
||||
install -Dm755 "basecamp" "${pkgdir}/usr/bin/basecamp"
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,48 @@
|
||||
# Maintainer: Ryan Hughes <ryan@omarchy.org>
|
||||
# Contributor: Bart De Vries <bart at mogwai dot be>
|
||||
# Contributor: Dan Johansen <strit@manjaro.org>
|
||||
#
|
||||
# Runs x86_64-only Linux programs (dropbox) on AArch64. Pinned past v0.4.5-1:
|
||||
# that release crashes in its glib wrapper as soon as Dropbox starts its tray
|
||||
# icon. Move to a release tag once one includes the pinned commit.
|
||||
|
||||
pkgname=box64
|
||||
pkgver=0.4.5.1.r309.gd58d619
|
||||
pkgrel=1
|
||||
_commit=d58d619e84e03282326e8a26c2cbfe749931b5f8
|
||||
pkgdesc='Linux userspace x86_64 emulator with native library wrapping'
|
||||
arch=('aarch64')
|
||||
url='https://github.com/ptitSeb/box64'
|
||||
license=('MIT')
|
||||
depends=('gcc-libs' 'glibc')
|
||||
makedepends=('cmake' 'python')
|
||||
backup=('etc/box64.box64rc')
|
||||
options=('!strip' '!emptydirs')
|
||||
source=("$pkgname-$_commit.tar.gz::$url/archive/$_commit.tar.gz")
|
||||
sha256sums=('f7615a3c4ac5412a983a3fd26be79311e6e2702884d8bdfde954b23bad39fff8')
|
||||
|
||||
build() {
|
||||
# ARM64 is box64's generic AArch64 profile, so the binary does not depend
|
||||
# on the CPU of the machine that built it.
|
||||
cmake -S "$pkgname-$_commit" -B build \
|
||||
-DARM64=ON \
|
||||
-DARM_DYNAREC=ON \
|
||||
-DNOGIT=ON \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr
|
||||
cmake --build build --parallel
|
||||
}
|
||||
|
||||
package() {
|
||||
DESTDIR="$pkgdir" cmake --install build
|
||||
|
||||
# Packages call box64 explicitly. A binfmt handler would compete with
|
||||
# qemu-user-static-binfmt for every x86_64 executable on the system.
|
||||
rm -r "$pkgdir/etc/binfmt.d"
|
||||
# The Qt rcfile editor needs PySide, and its launcher entry would show up
|
||||
# on every system that installs box64 only as a dependency.
|
||||
rm "$pkgdir/usr/bin/box64-configurator" \
|
||||
"$pkgdir/usr/share/applications/box64-configurator.desktop"
|
||||
|
||||
install -Dm644 "$pkgname-$_commit/LICENSE" -t "$pkgdir/usr/share/licenses/$pkgname"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"debian": "https://brave-browser-apt-release.s3.brave.com/dists/stable/main/binary-amd64/Packages",
|
||||
"package": "brave-browser",
|
||||
"pattern": "(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "brave-bin",
|
||||
"commit": "d86e5479e163dac19d7ace3de472710e6eb06eea"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# Maintainer: brave <aur-release@brave.com>
|
||||
# Contributor: Caleb Maclennan <caleb@alerque.com>
|
||||
# Contributor: José Miguel Sarasola <jmsaraur@gmail.com>
|
||||
# Contributor: Như Bảo Trương <28810481+nhubaotruong@users.noreply.github.com>
|
||||
# Contributor: Andrés Rodríguez <hello@andres.codes>
|
||||
# Contributor: Jacob Mischka <jacob@mischka.me>
|
||||
# Contributor: Manuel Mazzuola <origin.of@gmail.com>
|
||||
# Contributor: Simón Oroño <simonorono@protonmail.com>
|
||||
# Contributor: now-im <now im 627 @ gmail . com>
|
||||
# Contributor: Giusy Digital <kurmikon at libero dot it>
|
||||
|
||||
pkgname=brave-bin
|
||||
pkgver=1.96.61
|
||||
pkgrel=2
|
||||
epoch=1
|
||||
pkgdesc='Web browser that blocks ads and trackers by default (binary release)'
|
||||
arch=(x86_64 aarch64)
|
||||
url=https://brave.com
|
||||
license=(MPL2 BSD custom:chromium)
|
||||
depends=(alsa-lib
|
||||
gtk3
|
||||
libxss
|
||||
nss
|
||||
ttf-font)
|
||||
optdepends=('cups: Printer support'
|
||||
'libgnome-keyring: Enable GNOME keyring support'
|
||||
'libnotify: Native notification support')
|
||||
provides=("${pkgname%-bin}=$pkgver" 'brave-browser')
|
||||
conflicts=("${pkgname%-bin}")
|
||||
options=(!strip)
|
||||
source=($pkgname.sh brave-browser.desktop)
|
||||
source_x86_64=(${pkgname}-${pkgver}-x86_64.zip::https://github.com/brave/brave-browser/releases/download/v${pkgver}/brave-browser-${pkgver}-linux-amd64.zip)
|
||||
source_aarch64=(${pkgname}-${pkgver}-aarch64.zip::https://github.com/brave/brave-browser/releases/download/v${pkgver}/brave-browser-${pkgver}-linux-arm64.zip)
|
||||
|
||||
noextract=(${pkgname}-${pkgver}-x86_64.zip ${pkgname}-${pkgver}-aarch64.zip)
|
||||
sha256sums=('75a87dd17b42fcc6f27adfd16c82bed1c08e9251b07d2012f8d49f7412fa1d00'
|
||||
'c07276b69c7304981525ecb022f92daf7ae125a4fb05ac3442157b50826e257a')
|
||||
sha256sums_x86_64=('c68cf179603470e8001a949294fe98b593f0749ca95f42687d855081b1c394a4')
|
||||
sha256sums_aarch64=('33a1513379505642e4df0dda36a1dd78f735ee351631cffd4a4ac90b1cec0cd5')
|
||||
|
||||
prepare() {
|
||||
mkdir -p brave
|
||||
bsdtar -xf "$pkgname-$pkgver-$CARCH.zip" -C brave
|
||||
chmod +x brave/brave
|
||||
}
|
||||
|
||||
package() {
|
||||
install -dm0755 "$pkgdir/opt"
|
||||
cp -a brave "$pkgdir/opt/$pkgname"
|
||||
|
||||
# allow firejail users to get the suid sandbox working
|
||||
chmod 4755 "$pkgdir/opt/brave-bin/chrome-sandbox"
|
||||
|
||||
install -Dm0755 "$pkgname.sh" "$pkgdir/usr/bin/brave"
|
||||
install -Dm0644 -t "$pkgdir/usr/share/applications/" "brave-browser.desktop"
|
||||
install -Dm0644 -t "$pkgdir/usr/share/licenses/$pkgname/" brave/LICENSE
|
||||
pushd "$pkgdir/usr/"
|
||||
for size in 16x16 24x24 32x32 48x48 64x64 128x128 256x256; do
|
||||
install -Dm0644 "$pkgdir/opt/$pkgname/product_logo_${size/x*/}.png" \
|
||||
"share/icons/hicolor/$size/apps/brave-desktop.png"
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-"${HOME}/.config"}"
|
||||
|
||||
CONF_FILE="${XDG_CONFIG_HOME}/brave-flags.conf"
|
||||
|
||||
if
|
||||
test -f "${CONF_FILE}"
|
||||
then
|
||||
mapfile -t CONF_LIST < "${CONF_FILE}"
|
||||
fi
|
||||
|
||||
for CONF_LINE in "${CONF_LIST[@]}"
|
||||
do
|
||||
if ! [[
|
||||
"${CONF_LINE}" =~ ^[[:space:]]*(#|$)
|
||||
]]
|
||||
then
|
||||
FLAG_LIST+=("${CONF_LINE}")
|
||||
fi
|
||||
done
|
||||
|
||||
export CHROME_VERSION_EXTRA='stable'
|
||||
|
||||
exec /opt/brave-bin/brave "${FLAG_LIST[@]}" "${@}"
|
||||
@@ -0,0 +1,224 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Brave
|
||||
# Only KDE 4 seems to use GenericName, so we reuse the KDE strings.
|
||||
# From Ubuntu's language-pack-kde-XX-base packages, version 9.04-20090413.
|
||||
GenericName=Web Browser
|
||||
GenericName[ar]=متصفح الشبكة
|
||||
GenericName[bg]=Уеб браузър
|
||||
GenericName[ca]=Navegador web
|
||||
GenericName[cs]=WWW prohlížeč
|
||||
GenericName[da]=Browser
|
||||
GenericName[de]=Web-Browser
|
||||
GenericName[el]=Περιηγητής ιστού
|
||||
GenericName[en_GB]=Web Browser
|
||||
GenericName[es]=Navegador web
|
||||
GenericName[et]=Veebibrauser
|
||||
GenericName[fi]=WWW-selain
|
||||
GenericName[fr]=Navigateur Web
|
||||
GenericName[gu]=વેબ બ્રાઉઝર
|
||||
GenericName[he]=דפדפן אינטרנט
|
||||
GenericName[hi]=वेब ब्राउज़र
|
||||
GenericName[hu]=Webböngésző
|
||||
GenericName[it]=Browser Web
|
||||
GenericName[ja]=ウェブブラウザ
|
||||
GenericName[kn]=ಜಾಲ ವೀಕ್ಷಕ
|
||||
GenericName[ko]=웹 브라우저
|
||||
GenericName[lt]=Žiniatinklio naršyklė
|
||||
GenericName[lv]=Tīmekļa pārlūks
|
||||
GenericName[ml]=വെബ് ബ്രൌസര്
|
||||
GenericName[mr]=वेब ब्राऊजर
|
||||
GenericName[nb]=Nettleser
|
||||
GenericName[nl]=Webbrowser
|
||||
GenericName[pl]=Przeglądarka WWW
|
||||
GenericName[pt]=Navegador Web
|
||||
GenericName[pt_BR]=Navegador da Internet
|
||||
GenericName[ro]=Navigator de Internet
|
||||
GenericName[ru]=Веб-браузер
|
||||
GenericName[sl]=Spletni brskalnik
|
||||
GenericName[sv]=Webbläsare
|
||||
GenericName[ta]=இணைய உலாவி
|
||||
GenericName[th]=เว็บเบราว์เซอร์
|
||||
GenericName[tr]=Web Tarayıcı
|
||||
GenericName[uk]=Навігатор Тенет
|
||||
GenericName[zh_CN]=网页浏览器
|
||||
GenericName[zh_HK]=網頁瀏覽器
|
||||
GenericName[zh_TW]=網頁瀏覽器
|
||||
# Not translated in KDE, from Epiphany 2.26.1-0ubuntu1.
|
||||
GenericName[bn]=ওয়েব ব্রাউজার
|
||||
GenericName[fil]=Web Browser
|
||||
GenericName[hr]=Web preglednik
|
||||
GenericName[id]=Browser Web
|
||||
GenericName[or]=ଓ୍ବେବ ବ୍ରାଉଜର
|
||||
GenericName[sk]=WWW prehliadač
|
||||
GenericName[sr]=Интернет прегледник
|
||||
GenericName[te]=మహాతల అన్వేషి
|
||||
GenericName[vi]=Bộ duyệt Web
|
||||
# Gnome and KDE 3 uses Comment.
|
||||
Comment=Access the Internet
|
||||
Comment[ar]=الدخول إلى الإنترنت
|
||||
Comment[bg]=Достъп до интернет
|
||||
Comment[bn]=ইন্টারনেটটি অ্যাক্সেস করুন
|
||||
Comment[ca]=Accedeix a Internet
|
||||
Comment[cs]=Přístup k internetu
|
||||
Comment[da]=Få adgang til internettet
|
||||
Comment[de]=Internetzugriff
|
||||
Comment[el]=Πρόσβαση στο Διαδίκτυο
|
||||
Comment[en_GB]=Access the Internet
|
||||
Comment[es]=Accede a Internet.
|
||||
Comment[et]=Pääs Internetti
|
||||
Comment[fi]=Käytä internetiä
|
||||
Comment[fil]=I-access ang Internet
|
||||
Comment[fr]=Accéder à Internet
|
||||
Comment[gu]=ઇંટરનેટ ઍક્સેસ કરો
|
||||
Comment[he]=גישה אל האינטרנט
|
||||
Comment[hi]=इंटरनेट तक पहुंच स्थापित करें
|
||||
Comment[hr]=Pristup Internetu
|
||||
Comment[hu]=Internetelérés
|
||||
Comment[id]=Akses Internet
|
||||
Comment[it]=Accesso a Internet
|
||||
Comment[ja]=インターネットにアクセス
|
||||
Comment[kn]=ಇಂಟರ್ನೆಟ್ ಅನ್ನು ಪ್ರವೇಶಿಸಿ
|
||||
Comment[ko]=인터넷 연결
|
||||
Comment[lt]=Interneto prieiga
|
||||
Comment[lv]=Piekļūt internetam
|
||||
Comment[ml]=ഇന്റര്നെറ്റ് ആക്സസ് ചെയ്യുക
|
||||
Comment[mr]=इंटरनेटमध्ये प्रवेश करा
|
||||
Comment[nb]=Gå til Internett
|
||||
Comment[nl]=Verbinding maken met internet
|
||||
Comment[or]=ଇଣ୍ଟର୍ନେଟ୍ ପ୍ରବେଶ କରନ୍ତୁ
|
||||
Comment[pl]=Skorzystaj z internetu
|
||||
Comment[pt]=Aceder à Internet
|
||||
Comment[pt_BR]=Acessar a internet
|
||||
Comment[ro]=Accesaţi Internetul
|
||||
Comment[ru]=Доступ в Интернет
|
||||
Comment[sk]=Prístup do siete Internet
|
||||
Comment[sl]=Dostop do interneta
|
||||
Comment[sr]=Приступите Интернету
|
||||
Comment[sv]=Gå ut på Internet
|
||||
Comment[ta]=இணையத்தை அணுகுதல்
|
||||
Comment[te]=ఇంటర్నెట్ను ఆక్సెస్ చెయ్యండి
|
||||
Comment[th]=เข้าถึงอินเทอร์เน็ต
|
||||
Comment[tr]=İnternet'e erişin
|
||||
Comment[uk]=Доступ до Інтернету
|
||||
Comment[vi]=Truy cập Internet
|
||||
Comment[zh_CN]=访问互联网
|
||||
Comment[zh_HK]=連線到網際網路
|
||||
Comment[zh_TW]=連線到網際網路
|
||||
StartupNotify=true
|
||||
StartupWMClass=brave-browser
|
||||
TryExec=brave
|
||||
Exec=brave %U
|
||||
Terminal=false
|
||||
Icon=brave-desktop
|
||||
Type=Application
|
||||
Categories=Network;WebBrowser;
|
||||
MimeType=application/pdf;application/rdf+xml;application/rss+xml;application/xhtml+xml;application/xhtml_xml;application/xml;image/gif;image/jpeg;image/png;image/webp;text/html;text/xml;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/ipfs;x-scheme-handler/ipns;
|
||||
Actions=new-window;new-private-window;
|
||||
|
||||
[Desktop Action new-window]
|
||||
Name=New Window
|
||||
Name[am]=አዲስ መስኮት
|
||||
Name[ar]=نافذة جديدة
|
||||
Name[bg]=Нов прозорец
|
||||
Name[bn]=নতুন উইন্ডো
|
||||
Name[ca]=Finestra nova
|
||||
Name[cs]=Nové okno
|
||||
Name[da]=Nyt vindue
|
||||
Name[de]=Neues Fenster
|
||||
Name[el]=Νέο Παράθυρο
|
||||
Name[en_GB]=New Window
|
||||
Name[es]=Nueva ventana
|
||||
Name[et]=Uus aken
|
||||
Name[fa]=پنجره جدید
|
||||
Name[fi]=Uusi ikkuna
|
||||
Name[fil]=New Window
|
||||
Name[fr]=Nouvelle fenêtre
|
||||
Name[gu]=નવી વિંડો
|
||||
Name[hi]=नई विंडो
|
||||
Name[hr]=Novi prozor
|
||||
Name[hu]=Új ablak
|
||||
Name[id]=Jendela Baru
|
||||
Name[it]=Nuova finestra
|
||||
Name[iw]=חלון חדש
|
||||
Name[ja]=新規ウインドウ
|
||||
Name[kn]=ಹೊಸ ವಿಂಡೊ
|
||||
Name[ko]=새 창
|
||||
Name[lt]=Naujas langas
|
||||
Name[lv]=Jauns logs
|
||||
Name[ml]=പുതിയ വിന്ഡോ
|
||||
Name[mr]=नवीन विंडो
|
||||
Name[nl]=Nieuw venster
|
||||
Name[no]=Nytt vindu
|
||||
Name[pl]=Nowe okno
|
||||
Name[pt]=Nova janela
|
||||
Name[pt_BR]=Nova janela
|
||||
Name[ro]=Fereastră nouă
|
||||
Name[ru]=Новое окно
|
||||
Name[sk]=Nové okno
|
||||
Name[sl]=Novo okno
|
||||
Name[sr]=Нови прозор
|
||||
Name[sv]=Nytt fönster
|
||||
Name[sw]=Dirisha Jipya
|
||||
Name[ta]=புதிய சாளரம்
|
||||
Name[te]=క్రొత్త విండో
|
||||
Name[th]=หน้าต่างใหม่
|
||||
Name[tr]=Yeni Pencere
|
||||
Name[uk]=Нове вікно
|
||||
Name[vi]=Cửa sổ Mới
|
||||
Name[zh_CN]=新建窗口
|
||||
Name[zh_TW]=開新視窗
|
||||
Exec=brave
|
||||
|
||||
[Desktop Action new-private-window]
|
||||
Name=New Incognito Window
|
||||
Name[ar]=نافذة جديدة للتصفح المتخفي
|
||||
Name[bg]=Нов прозорец „инкогнито“
|
||||
Name[bn]=নতুন ছদ্মবেশী উইন্ডো
|
||||
Name[ca]=Finestra d'incògnit nova
|
||||
Name[cs]=Nové anonymní okno
|
||||
Name[da]=Nyt inkognitovindue
|
||||
Name[de]=Neues Inkognito-Fenster
|
||||
Name[el]=Νέο παράθυρο για ανώνυμη περιήγηση
|
||||
Name[en_GB]=New Incognito window
|
||||
Name[es]=Nueva ventana de incógnito
|
||||
Name[et]=Uus inkognito aken
|
||||
Name[fa]=پنجره جدید حالت ناشناس
|
||||
Name[fi]=Uusi incognito-ikkuna
|
||||
Name[fil]=Bagong Incognito window
|
||||
Name[fr]=Nouvelle fenêtre de navigation privée
|
||||
Name[gu]=નવી છુપી વિંડો
|
||||
Name[hi]=नई गुप्त विंडो
|
||||
Name[hr]=Novi anoniman prozor
|
||||
Name[hu]=Új Inkognitóablak
|
||||
Name[id]=Jendela Penyamaran baru
|
||||
Name[it]=Nuova finestra di navigazione in incognito
|
||||
Name[iw]=חלון חדש לגלישה בסתר
|
||||
Name[ja]=新しいシークレット ウィンドウ
|
||||
Name[kn]=ಹೊಸ ಅಜ್ಞಾತ ವಿಂಡೋ
|
||||
Name[ko]=새 시크릿 창
|
||||
Name[lt]=Naujas inkognito langas
|
||||
Name[lv]=Jauns inkognito režīma logs
|
||||
Name[ml]=പുതിയ വേഷ പ്രച്ഛന്ന വിന്ഡോ
|
||||
Name[mr]=नवीन गुप्त विंडो
|
||||
Name[nl]=Nieuw incognitovenster
|
||||
Name[no]=Nytt inkognitovindu
|
||||
Name[pl]=Nowe okno incognito
|
||||
Name[pt]=Nova janela de navegação anónima
|
||||
Name[pt_BR]=Nova janela anônima
|
||||
Name[ro]=Fereastră nouă incognito
|
||||
Name[ru]=Новое окно в режиме инкогнито
|
||||
Name[sk]=Nové okno inkognito
|
||||
Name[sl]=Novo okno brez beleženja zgodovine
|
||||
Name[sr]=Нови прозор за прегледање без архивирања
|
||||
Name[sv]=Nytt inkognitofönster
|
||||
Name[ta]=புதிய மறைநிலைச் சாளரம்
|
||||
Name[te]=క్రొత్త అజ్ఞాత విండో
|
||||
Name[th]=หน้าต่างใหม่ที่ไม่ระบุตัวตน
|
||||
Name[tr]=Yeni Gizli pencere
|
||||
Name[uk]=Нове вікно в режимі анонімного перегляду
|
||||
Name[vi]=Cửa sổ ẩn danh mới
|
||||
Name[zh_CN]=新建隐身窗口
|
||||
Name[zh_TW]=新增無痕式視窗
|
||||
Exec=brave --incognito
|
||||
MimeType=x-scheme-handler/unknown;x-scheme-handler/about;text/html;text/xml;application/xhtml_xml;image/webp;x-scheme-handler/http;x-scheme-handler/https;
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"debian": "https://brave-browser-apt-release.s3.brave.com/dists/stable/main/binary-amd64/Packages",
|
||||
"package": "brave-origin",
|
||||
"pattern": "(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "brave-origin-bin",
|
||||
"commit": "f2daecf7b7576cd445094ed09c3c783619ca0eb2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# Maintainer: brave <aur-release@brave.com>
|
||||
|
||||
pkgname=brave-origin-bin
|
||||
pkgver=1.96.61
|
||||
pkgrel=2
|
||||
epoch=1
|
||||
pkgdesc='The minimalist browser from the makers of Brave (binary release).'
|
||||
arch=(x86_64 aarch64)
|
||||
url=https://brave.com/origin/download
|
||||
license=('MPL2')
|
||||
depends=(alsa-lib gtk3 libxss nss ttf-font)
|
||||
optdepends=('cups: Printer support'
|
||||
'libgnome-keyring: Enable GNOME keyring support'
|
||||
'libnotify: Native notification support')
|
||||
provides=("$pkgname" "${pkgname%-bin}")
|
||||
conflicts=("${pkgname%-bin}")
|
||||
options=(!strip)
|
||||
source=($pkgname.sh "${pkgname%-bin}.desktop")
|
||||
source_x86_64=("${pkgname}-${pkgver}-x86_64.zip::https://github.com/brave/brave-browser/releases/download/v${pkgver}/brave-origin-${pkgver}-linux-amd64.zip")
|
||||
source_aarch64=("${pkgname}-${pkgver}-aarch64.zip::https://github.com/brave/brave-browser/releases/download/v${pkgver}/brave-origin-${pkgver}-linux-arm64.zip")
|
||||
|
||||
noextract=("${pkgname}-${pkgver}-x86_64.zip" "${pkgname}-${pkgver}-aarch64.zip")
|
||||
sha256sums=('5ff70ee473f35c2fc7642c422c8abe20aaac0d7cc30a3292744eb9fbeafba1bd'
|
||||
'c70bc71c696b6764247070375ae111bd76c8bad9c7bda4d46e03975b95571a8a')
|
||||
sha256sums_x86_64=('06e4c48b71c65a33bdb94b6909aab505237b5939adf8985b332cb14557792816')
|
||||
sha256sums_aarch64=('df07224b25284b67b5c7d0e8859d16f3d2533abcf588c81437df59e9fd4f4c62')
|
||||
|
||||
prepare() {
|
||||
mkdir -p brave
|
||||
bsdtar -xf "$pkgname-$pkgver-$CARCH.zip" -C brave
|
||||
chmod +x brave/brave
|
||||
}
|
||||
|
||||
package() {
|
||||
install -dm0755 "$pkgdir/opt"
|
||||
cp -a brave "$pkgdir/opt/$pkgname"
|
||||
chmod 4755 "$pkgdir/opt/$pkgname/chrome-sandbox"
|
||||
install -Dm0755 "$pkgname.sh" "$pkgdir/usr/bin/${pkgname%-bin}"
|
||||
install -Dm0644 -t "$pkgdir/usr/share/applications/" "${pkgname%-bin}.desktop"
|
||||
install -Dm0644 -t "$pkgdir/usr/share/licenses/$pkgname/" brave/LICENSE
|
||||
pushd "$pkgdir/usr/"
|
||||
for size in 16x16 24x24 32x32 48x48 64x64 128x128 256x256; do
|
||||
install -Dm0644 "$pkgdir/opt/$pkgname/product_logo_${size/x*/}.png" \
|
||||
"share/icons/hicolor/$size/apps/brave-origin.png"
|
||||
done
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-"${HOME}/.config"}"
|
||||
|
||||
CONF_FILE="${XDG_CONFIG_HOME}/brave-origin-flags.conf"
|
||||
|
||||
if
|
||||
test -f "${CONF_FILE}"
|
||||
then
|
||||
mapfile -t CONF_LIST < "${CONF_FILE}"
|
||||
fi
|
||||
|
||||
for CONF_LINE in "${CONF_LIST[@]}"
|
||||
do
|
||||
if ! [[
|
||||
"${CONF_LINE}" =~ ^[[:space:]]*(#|$)
|
||||
]]
|
||||
then
|
||||
FLAG_LIST+=("${CONF_LINE}")
|
||||
fi
|
||||
done
|
||||
|
||||
export CHROME_VERSION_EXTRA='stable'
|
||||
|
||||
exec /opt/brave-origin-bin/brave-origin "${FLAG_LIST[@]}" "${@}"
|
||||
@@ -0,0 +1,222 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Brave Origin
|
||||
# Only KDE 4 seems to use GenericName, so we reuse the KDE strings.
|
||||
# From Ubuntu's language-pack-kde-XX-base packages, version 9.04-20090413.
|
||||
GenericName=Web Browser
|
||||
GenericName[ar]=متصفح الشبكة
|
||||
GenericName[bg]=Уеб браузър
|
||||
GenericName[ca]=Navegador web
|
||||
GenericName[cs]=WWW prohlížeč
|
||||
GenericName[da]=Browser
|
||||
GenericName[de]=Web-Browser
|
||||
GenericName[el]=Περιηγητής ιστού
|
||||
GenericName[en_GB]=Web Browser
|
||||
GenericName[es]=Navegador web
|
||||
GenericName[et]=Veebibrauser
|
||||
GenericName[fi]=WWW-selain
|
||||
GenericName[fr]=Navigateur Web
|
||||
GenericName[gu]=વેબ બ્રાઉઝર
|
||||
GenericName[he]=דפדפן אינטרנט
|
||||
GenericName[hi]=वेब ब्राउज़र
|
||||
GenericName[hu]=Webböngésző
|
||||
GenericName[it]=Browser Web
|
||||
GenericName[ja]=ウェブブラウザ
|
||||
GenericName[kn]=ಜಾಲ ವೀಕ್ಷಕ
|
||||
GenericName[ko]=웹 브라우저
|
||||
GenericName[lt]=Žiniatinklio naršyklė
|
||||
GenericName[lv]=Tīmekļa pārlūks
|
||||
GenericName[ml]=വെബ് ബ്രൌസര്
|
||||
GenericName[mr]=वेब ब्राऊजर
|
||||
GenericName[nb]=Nettleser
|
||||
GenericName[nl]=Webbrowser
|
||||
GenericName[pl]=Przeglądarka WWW
|
||||
GenericName[pt]=Navegador Web
|
||||
GenericName[pt_BR]=Navegador da Internet
|
||||
GenericName[ro]=Navigator de Internet
|
||||
GenericName[ru]=Веб-браузер
|
||||
GenericName[sl]=Spletni brskalnik
|
||||
GenericName[sv]=Webbläsare
|
||||
GenericName[ta]=இணைய உலாவி
|
||||
GenericName[th]=เว็บเบราว์เซอร์
|
||||
GenericName[tr]=Web Tarayıcı
|
||||
GenericName[uk]=Навігатор Тенет
|
||||
GenericName[zh_CN]=网页浏览器
|
||||
GenericName[zh_HK]=網頁瀏覽器
|
||||
GenericName[zh_TW]=網頁瀏覽器
|
||||
# Not translated in KDE, from Epiphany 2.26.1-0ubuntu1.
|
||||
GenericName[bn]=ওয়েব ব্রাউজার
|
||||
GenericName[fil]=Web Browser
|
||||
GenericName[hr]=Web preglednik
|
||||
GenericName[id]=Browser Web
|
||||
GenericName[or]=ଓ୍ବେବ ବ୍ରାଉଜର
|
||||
GenericName[sk]=WWW prehliadač
|
||||
GenericName[sr]=Интернет прегледник
|
||||
GenericName[te]=మహాతల అన్వేషి
|
||||
GenericName[vi]=Bộ duyệt Web
|
||||
# Gnome and KDE 3 uses Comment.
|
||||
Comment=Access the Internet
|
||||
Comment[ar]=الدخول إلى الإنترنت
|
||||
Comment[bg]=Достъп до интернет
|
||||
Comment[bn]=ইন্টারনেটটি অ্যাক্সেস করুন
|
||||
Comment[ca]=Accedeix a Internet
|
||||
Comment[cs]=Přístup k internetu
|
||||
Comment[da]=Få adgang til internettet
|
||||
Comment[de]=Internetzugriff
|
||||
Comment[el]=Πρόσβαση στο Διαδίκτυο
|
||||
Comment[en_GB]=Access the Internet
|
||||
Comment[es]=Accede a Internet.
|
||||
Comment[et]=Pääs Internetti
|
||||
Comment[fi]=Käytä internetiä
|
||||
Comment[fil]=I-access ang Internet
|
||||
Comment[fr]=Accéder à Internet
|
||||
Comment[gu]=ઇંટરનેટ ઍક્સેસ કરો
|
||||
Comment[he]=גישה אל האינטרנט
|
||||
Comment[hi]=इंटरनेट तक पहुंच स्थापित करें
|
||||
Comment[hr]=Pristup Internetu
|
||||
Comment[hu]=Internetelérés
|
||||
Comment[id]=Akses Internet
|
||||
Comment[it]=Accesso a Internet
|
||||
Comment[ja]=インターネットにアクセス
|
||||
Comment[kn]=ಇಂಟರ್ನೆಟ್ ಅನ್ನು ಪ್ರವೇಶಿಸಿ
|
||||
Comment[ko]=인터넷 연결
|
||||
Comment[lt]=Interneto prieiga
|
||||
Comment[lv]=Piekļūt internetam
|
||||
Comment[ml]=ഇന്റര്നെറ്റ് ആക്സസ് ചെയ്യുക
|
||||
Comment[mr]=इंटरनेटमध्ये प्रवेश करा
|
||||
Comment[nb]=Gå til Internett
|
||||
Comment[nl]=Verbinding maken met internet
|
||||
Comment[or]=ଇଣ୍ଟର୍ନେଟ୍ ପ୍ରବେଶ କରନ୍ତୁ
|
||||
Comment[pl]=Skorzystaj z internetu
|
||||
Comment[pt]=Aceder à Internet
|
||||
Comment[pt_BR]=Acessar a internet
|
||||
Comment[ro]=Accesaţi Internetul
|
||||
Comment[ru]=Доступ в Интернет
|
||||
Comment[sk]=Prístup do siete Internet
|
||||
Comment[sl]=Dostop do interneta
|
||||
Comment[sr]=Приступите Интернету
|
||||
Comment[sv]=Gå ut på Internet
|
||||
Comment[ta]=இணையத்தை அணுகுதல்
|
||||
Comment[te]=ఇంటర్నెట్ను ఆక్సెస్ చెయ్యండి
|
||||
Comment[th]=เข้าถึงอินเทอร์เน็ต
|
||||
Comment[tr]=İnternet'e erişin
|
||||
Comment[uk]=Доступ до Інтернету
|
||||
Comment[vi]=Truy cập Internet
|
||||
Comment[zh_CN]=访问互联网
|
||||
Comment[zh_HK]=連線到網際網路
|
||||
Comment[zh_TW]=連線到網際網路
|
||||
StartupNotify=true
|
||||
StartupWMClass=brave-origin
|
||||
Exec=brave-origin %U
|
||||
Terminal=false
|
||||
Icon=brave-origin
|
||||
Type=Application
|
||||
Categories=Network;WebBrowser;
|
||||
MimeType=application/pdf;application/rdf+xml;application/rss+xml;application/xhtml+xml;application/xhtml_xml;application/xml;image/gif;image/jpeg;image/png;image/webp;text/html;text/xml;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/chromium;
|
||||
Actions=new-window;new-private-window;
|
||||
|
||||
[Desktop Action new-window]
|
||||
Name=New Window
|
||||
Name[am]=አዲስ መስኮት
|
||||
Name[ar]=نافذة جديدة
|
||||
Name[bg]=Нов прозорец
|
||||
Name[bn]=নতুন উইন্ডো
|
||||
Name[ca]=Finestra nova
|
||||
Name[cs]=Nové okno
|
||||
Name[da]=Nyt vindue
|
||||
Name[de]=Neues Fenster
|
||||
Name[el]=Νέο Παράθυρο
|
||||
Name[en_GB]=New Window
|
||||
Name[es]=Nueva ventana
|
||||
Name[et]=Uus aken
|
||||
Name[fa]=پنجره جدید
|
||||
Name[fi]=Uusi ikkuna
|
||||
Name[fil]=New Window
|
||||
Name[fr]=Nouvelle fenêtre
|
||||
Name[gu]=નવી વિંડો
|
||||
Name[hi]=नई विंडो
|
||||
Name[hr]=Novi prozor
|
||||
Name[hu]=Új ablak
|
||||
Name[id]=Jendela Baru
|
||||
Name[it]=Nuova finestra
|
||||
Name[iw]=חלון חדש
|
||||
Name[ja]=新規ウインドウ
|
||||
Name[kn]=ಹೊಸ ವಿಂಡೊ
|
||||
Name[ko]=새 창
|
||||
Name[lt]=Naujas langas
|
||||
Name[lv]=Jauns logs
|
||||
Name[ml]=പുതിയ വിന്ഡോ
|
||||
Name[mr]=नवीन विंडो
|
||||
Name[nl]=Nieuw venster
|
||||
Name[no]=Nytt vindu
|
||||
Name[pl]=Nowe okno
|
||||
Name[pt]=Nova janela
|
||||
Name[pt_BR]=Nova janela
|
||||
Name[ro]=Fereastră nouă
|
||||
Name[ru]=Новое окно
|
||||
Name[sk]=Nové okno
|
||||
Name[sl]=Novo okno
|
||||
Name[sr]=Нови прозор
|
||||
Name[sv]=Nytt fönster
|
||||
Name[sw]=Dirisha Jipya
|
||||
Name[ta]=புதிய சாளரம்
|
||||
Name[te]=క్రొత్త విండో
|
||||
Name[th]=หน้าต่างใหม่
|
||||
Name[tr]=Yeni Pencere
|
||||
Name[uk]=Нове вікно
|
||||
Name[vi]=Cửa sổ Mới
|
||||
Name[zh_CN]=新建窗口
|
||||
Name[zh_TW]=開新視窗
|
||||
Exec=brave-origin
|
||||
|
||||
[Desktop Action new-private-window]
|
||||
Name=New Incognito Window
|
||||
Name[ar]=نافذة جديدة للتصفح المتخفي
|
||||
Name[bg]=Нов прозорец „инкогнито“
|
||||
Name[bn]=নতুন ছদ্মবেশী উইন্ডো
|
||||
Name[ca]=Finestra d'incògnit nova
|
||||
Name[cs]=Nové anonymní okno
|
||||
Name[da]=Nyt inkognitovindue
|
||||
Name[de]=Neues Inkognito-Fenster
|
||||
Name[el]=Νέο παράθυρο για ανώνυμη περιήγηση
|
||||
Name[en_GB]=New Incognito window
|
||||
Name[es]=Nueva ventana de incógnito
|
||||
Name[et]=Uus inkognito aken
|
||||
Name[fa]=پنجره جدید حالت ناشناس
|
||||
Name[fi]=Uusi incognito-ikkuna
|
||||
Name[fil]=Bagong Incognito window
|
||||
Name[fr]=Nouvelle fenêtre de navigation privée
|
||||
Name[gu]=નવી છુપી વિંડો
|
||||
Name[hi]=नई गुप्त विंडो
|
||||
Name[hr]=Novi anoniman prozor
|
||||
Name[hu]=Új Inkognitóablak
|
||||
Name[id]=Jendela Penyamaran baru
|
||||
Name[it]=Nuova finestra di navigazione in incognito
|
||||
Name[iw]=חלון חדש לגלישה בסתר
|
||||
Name[ja]=新しいシークレット ウィンドウ
|
||||
Name[kn]=ಹೊಸ ಅಜ್ಞಾತ ವಿಂಡೋ
|
||||
Name[ko]=새 시크릿 창
|
||||
Name[lt]=Naujas inkognito langas
|
||||
Name[lv]=Jauns inkognito režīma logs
|
||||
Name[ml]=പുതിയ വേഷ പ്രച്ഛന്ന വിന്ഡോ
|
||||
Name[mr]=नवीन गुप्त विंडो
|
||||
Name[nl]=Nieuw incognitovenster
|
||||
Name[no]=Nytt inkognitovindu
|
||||
Name[pl]=Nowe okno incognito
|
||||
Name[pt]=Nova janela de navegação anónima
|
||||
Name[pt_BR]=Nova janela anônima
|
||||
Name[ro]=Fereastră nouă incognito
|
||||
Name[ru]=Новое окно в режиме инкогнито
|
||||
Name[sk]=Nové okno inkognito
|
||||
Name[sl]=Novo okno brez beleženja zgodovine
|
||||
Name[sr]=Нови прозор за прегледање без архивирања
|
||||
Name[sv]=Nytt inkognitofönster
|
||||
Name[ta]=புதிய மறைநிலைச் சாளரம்
|
||||
Name[te]=క్రొత్త అజ్ఞాత విండో
|
||||
Name[th]=หน้าต่างใหม่ที่ไม่ระบุตัวตน
|
||||
Name[tr]=Yeni Gizli pencere
|
||||
Name[uk]=Нове вікно в режимі анонімного перегляду
|
||||
Name[vi]=Cửa sổ ẩn danh mới
|
||||
Name[zh_CN]=新建隐身窗口
|
||||
Name[zh_TW]=新增無痕式視窗
|
||||
Exec=brave-origin --incognito
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "195b828d52ce9a181215f753927123e7ef2af252"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"github": "oven-sh/bun",
|
||||
"pattern": "bun-v(?P<version>[0-9]+(?:\\.[0-9]+)*)"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "bun-bin",
|
||||
"commit": "195b828d52ce9a181215f753927123e7ef2af252"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
# Contributor: 37h4n (aarch64 support added by Ethan Reece <aur at ethanreece dot com>)
|
||||
# Contributor: sh!zeeg (shizeeque@gmail.com) support for non-avx2 CPUs, shell completions.
|
||||
pkgname=bun-bin
|
||||
pkgver=1.3.11
|
||||
pkgver=1.4.2
|
||||
pkgrel=1
|
||||
pkgdesc="All-in-one JavaScript runtime built for speed, with bundler, transpiler, test runner, and package manager. Includes bunx, shell completions and support for baseline CPUs"
|
||||
arch=('x86_64' 'aarch64')
|
||||
@@ -12,11 +12,8 @@ license=('MIT')
|
||||
provides=('bun')
|
||||
conflicts=('bun')
|
||||
options=('!debug')
|
||||
sha256sums_x86_64=('8611ba935af886f05a6f38740a15160326c15e5d5d07adef966130b4493607ed'
|
||||
'abe346f63414547cdf6b35b7a649a490c728b93d006226156923918a84c0e59b'
|
||||
'9b296bcc20090b5ea079dc1bca15913a32246121169bcf54cbdb7384d6f5b32b')
|
||||
sha256sums_aarch64=('d13944da12a53ecc74bf6a720bd1d04c4555c038dfe422365356a7be47691fdf'
|
||||
'9b296bcc20090b5ea079dc1bca15913a32246121169bcf54cbdb7384d6f5b32b')
|
||||
sha256sums_x86_64=('36368faef7527875d5ffa52e53cd48021741f2a83eb6208a8dd64068d422a913' 'c678040f14fe0440eb839d37cbd0ce4c051a32da72806ac97de6a6aab6bf728f' '9b296bcc20090b5ea079dc1bca15913a32246121169bcf54cbdb7384d6f5b32b')
|
||||
sha256sums_aarch64=('54328bbc2d9c8e0c9f892c544d66c57a83b84139e34909e5ee81758f1ac8fda7' '9b296bcc20090b5ea079dc1bca15913a32246121169bcf54cbdb7384d6f5b32b')
|
||||
source_x86_64=(
|
||||
"bun-x64.zip::https://github.com/oven-sh/bun/releases/download/bun-v${pkgver}/bun-linux-x64.zip"
|
||||
"bun-x64-baseline.zip::https://github.com/oven-sh/bun/releases/download/bun-v${pkgver}/bun-linux-x64-baseline.zip"
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"source": "aur",
|
||||
"source": "local",
|
||||
"release_ring": "fast",
|
||||
"upstream_commit": "558b3f7387063304f7392f839c6b693508976338"
|
||||
"upstream": {
|
||||
"watch": {
|
||||
"regex": "https://downloads.claude.ai/claude-code-releases/latest",
|
||||
"pattern": "^(?P<version>[0-9]+(?:\\.[0-9]+)*)\\s*$"
|
||||
}
|
||||
},
|
||||
"origin": {
|
||||
"aur": "claude-code",
|
||||
"commit": "27f61daa48ebdca87c9b2c7c83c162100d233ccc"
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
# Automation repository: https://github.com/fabifont/claude-code-aur
|
||||
|
||||
pkgname=claude-code
|
||||
pkgver=2.1.247
|
||||
pkgrel=1
|
||||
pkgver=2.1.291
|
||||
pkgrel=2
|
||||
pkgdesc="An agentic coding tool that lives in your terminal"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://github.com/anthropics/claude-code"
|
||||
@@ -29,8 +29,8 @@ source_x86_64=("claude-${pkgver}-x86_64::https://downloads.claude.ai/claude-code
|
||||
source_aarch64=("claude-${pkgver}-aarch64::https://downloads.claude.ai/claude-code-releases/${pkgver}/linux-arm64/claude")
|
||||
|
||||
sha256sums=('SKIP')
|
||||
sha256sums_x86_64=('5fb321bf417ffc5cd4e3f36e7c9c7e029bf47aaa36d5621db979fcc5e6eabe15')
|
||||
sha256sums_aarch64=('a68bb633f85e4a0e73465952ea624cf18992c3fa4db615feeb942ffad57d082a')
|
||||
sha256sums_x86_64=('078fad28d0297c9a25d306b635b2d8816c6839347520f29eb54ffea5d56142fb')
|
||||
sha256sums_aarch64=('c18473a04cc4f077435d5d9081f09ebea46e699eb2825cea64741c4bccb87647')
|
||||
|
||||
package() {
|
||||
install -Dm755 "${srcdir}/claude-${pkgver}-${CARCH}" "${pkgdir}/opt/claude-code/bin/claude"
|
||||
|
||||
Loaded 100 of 1560 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user