Compare commits
900
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d47199f2f2 | ||
|
|
66ef55ded6 | ||
|
|
8aa51cdc5b | ||
|
|
f5f516d56d | ||
|
|
f3e477be9f | ||
|
|
c17afe1706 | ||
|
|
c82acec7a7 | ||
|
|
9c26c9316a | ||
|
|
e470fa493d | ||
|
|
c01894c8b8 | ||
|
|
e52f779dfe | ||
|
|
67569bb16f | ||
|
|
b2e05dfe57 | ||
|
|
e5852b6d4a | ||
|
|
a3db46e792 | ||
|
|
40ea1555f2 | ||
|
|
60551aaddc | ||
|
|
d5406b5cb0 | ||
|
|
68a2c1fb4d | ||
|
|
9c81c8a705 | ||
|
|
d0815d6339 | ||
|
|
175c605260 | ||
|
|
be8f68c212 | ||
|
|
24a7358c17 | ||
|
|
556ab26156 | ||
|
|
93271007bb | ||
|
|
90636164c1 | ||
|
|
efa2f210bc | ||
|
|
c61e5e472f | ||
|
|
f854e042a6 | ||
|
|
41c7c2d948 | ||
|
|
99dd4771b5 | ||
|
|
c132fcde07 | ||
|
|
6645e01922 | ||
|
|
9fe85272cb | ||
|
|
81d8b8c525 | ||
|
|
06d43b0f6f | ||
|
|
1b75e24451 | ||
|
|
ce44914763 | ||
|
|
d58cb276a2 | ||
|
|
9e28d35c86 | ||
|
|
d0c84b5919 | ||
|
|
b066f816bd | ||
|
|
f02f02b3a8 | ||
|
|
5566be3e01 | ||
|
|
77c2c9807b | ||
|
|
356f7c657e | ||
|
|
81670c4a4e | ||
|
|
c1581a8cbb | ||
|
|
4997d4beb4 | ||
|
|
effdae0b6a | ||
|
|
bc25acb404 | ||
|
|
1161db0816 | ||
|
|
d331905a0c | ||
|
|
ef38c10e58 | ||
|
|
51c492e708 | ||
|
|
2b04aa5bf6 | ||
|
|
7aa734313d | ||
|
|
f14c5d206d | ||
|
|
59b1dd0a08 | ||
|
|
db4f3d9d2c | ||
|
|
0a906801f1 | ||
|
|
040315b98f | ||
|
|
5fcf959788 | ||
|
|
b1d7399222 | ||
|
|
2d0e3b510a | ||
|
|
8787c23f03 | ||
|
|
c0ff0692f0 | ||
|
|
306ac722f5 | ||
|
|
e79a9fbda8 | ||
|
|
cec136fe20 | ||
|
|
703e9d5fbc | ||
|
|
3c27563492 | ||
|
|
c2166632e9 | ||
|
|
859c6ff154 | ||
|
|
0a0c01a205 | ||
|
|
f54d045df3 | ||
|
|
39ed2398ab | ||
|
|
cfd1b9a554 | ||
|
|
011a14a0d6 | ||
|
|
4936892061 | ||
|
|
8a9b0f70ff | ||
|
|
eb597ed851 | ||
|
|
7dde80c9f7 | ||
|
|
6bbdcb8285 | ||
|
|
7f010b273e | ||
|
|
0aa396dce2 | ||
|
|
a268d610a3 | ||
|
|
80ebf43ed9 | ||
|
|
e6ca59be3a | ||
|
|
1aa8c1a886 | ||
|
|
031fdcd972 | ||
|
|
16d6e44ad9 | ||
|
|
42aae5ee2c | ||
|
|
d764b4f7f4 | ||
|
|
29836c6da9 | ||
|
|
39f0096f35 | ||
|
|
473a6f12ea | ||
|
|
d2cf8ee83e | ||
|
|
7fc218f0dd | ||
|
|
934eed2d7e | ||
|
|
fc1d4b44dc | ||
|
|
9743e7654c | ||
|
|
ba7561a34c | ||
|
|
a48d48cc04 | ||
|
|
a7f730d1e6 | ||
|
|
76c1de2185 | ||
|
|
1cef67f336 | ||
|
|
e60fd71366 | ||
|
|
c1257946a3 | ||
|
|
d32f520cb9 | ||
|
|
0cda546059 | ||
|
|
88947c367b | ||
|
|
c104b20285 | ||
|
|
cbded97503 | ||
|
|
8b8ede46ed | ||
|
|
1aecf94f22 | ||
|
|
bf0af463ef | ||
|
|
9ea749835f | ||
|
|
69c684305e | ||
|
|
7e86af28b3 | ||
|
|
e34944e9fb | ||
|
|
d1f4696163 | ||
|
|
e3730916ff | ||
|
|
7fa74a3369 | ||
|
|
f3e71f8a13 | ||
|
|
3e4cb2308e | ||
|
|
a090ebeb4b | ||
|
|
26839faa98 | ||
|
|
2684cc04c7 | ||
|
|
90066fc19b | ||
|
|
83778ddcb0 | ||
|
|
ae5eac6532 | ||
|
|
5c6444e604 | ||
|
|
17ae27fb21 | ||
|
|
086d1e1336 | ||
|
|
9b7daaf564 | ||
|
|
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 | ||
|
|
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,76 @@
|
|||||||
|
// 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.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (!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
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Usage: package-pr-groups.sh BRANCH_PREFIX PATCH_OUT
|
||||||
|
#
|
||||||
|
# Run from the repository root after a sync has rewritten recipes in the
|
||||||
|
# working tree. Writes those changes to PATCH_OUT and prints, as one JSON
|
||||||
|
# line, the PRs they split into: [{"branch": ..., "packages": "a b"}, ...].
|
||||||
|
#
|
||||||
|
# One PR per package, so a package whose build fails holds back only itself.
|
||||||
|
# Packages pinned from the same upstream branch (omarchy-dev and
|
||||||
|
# omarchy-settings-dev) share a PR: sync-upstream moves them in lockstep, and
|
||||||
|
# merging one without the other would ship the pair from different commits.
|
||||||
|
# Branch names come from sync-pr-branch.sh, so a scheduled run and a dispatch
|
||||||
|
# naming the same packages update the same PR.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
prefix=${1:?branch prefix required}
|
||||||
|
patch=${2:?patch path required}
|
||||||
|
scripts=$(dirname "$0")
|
||||||
|
|
||||||
|
git diff --binary -- pkgbuilds > "$patch"
|
||||||
|
|
||||||
|
declare -A members=()
|
||||||
|
while IFS= read -r package; do
|
||||||
|
key=$(jq -r '(.upstream.watch? | objects | select(has("git_branch")) | "\(.git_branch)#\(.branch)") // empty' \
|
||||||
|
"pkgbuilds/$package/.omarchy/package.json" 2>/dev/null || true)
|
||||||
|
members["${key:-$package}"]+="$package "
|
||||||
|
done < <(git diff --name-only -- pkgbuilds | cut -d/ -f2 | sort -u)
|
||||||
|
|
||||||
|
groups='[]'
|
||||||
|
for key in "${!members[@]}"; do
|
||||||
|
read -r -a packages <<<"${members[$key]}"
|
||||||
|
branch=$("$scripts/sync-pr-branch.sh" "$prefix" "${packages[@]}" | sed -n 's/^branch=//p')
|
||||||
|
groups=$(jq -c --arg branch "$branch" --arg packages "${packages[*]}" \
|
||||||
|
'. + [{branch: $branch, packages: $packages}]' <<<"$groups")
|
||||||
|
done
|
||||||
|
jq -c 'sort_by(.branch)' <<<"$groups"
|
||||||
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,321 @@
|
|||||||
|
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 bots' branches
|
||||||
|
# (auto/sync-* and auto/track-branches-*) 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. The tracker labels the PR it opens, so a new tracker PR starts two
|
||||||
|
# runs at once; cancelling the first left a red `result` on the PR (#901).
|
||||||
|
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-') || startsWith(github.head_ref, 'auto/track-branches'))) }}
|
||||||
|
|
||||||
|
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,134 @@
|
|||||||
|
name: Open package PRs
|
||||||
|
|
||||||
|
# Shared by the three bot lanes (sync-upstream.yml, sync-rebuilds.yml and
|
||||||
|
# track-branches.yml). The caller's sync job rewrites recipes, groups the
|
||||||
|
# changes with .github/scripts/package-pr-groups.sh and uploads them as the
|
||||||
|
# `package-pins` artifact. This opens or updates one PR per group and arms
|
||||||
|
# auto-merge, so every green package lands on its own and only the failures
|
||||||
|
# stay open. A single batch PR let one red package hold back every other one
|
||||||
|
# (#788, #837).
|
||||||
|
#
|
||||||
|
# Everything goes through the PAT in PKGS_BOT_TOKEN: its pushes build without
|
||||||
|
# an approval hold, and a merge made with it starts publish.yml. A merge made
|
||||||
|
# with the built-in GITHUB_TOKEN would not publish.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
groups:
|
||||||
|
description: 'JSON from package-pr-groups.sh: [{"branch", "packages"}]'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
prefix:
|
||||||
|
description: 'Branch prefix the caller owns, e.g. auto/sync-upstream'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
description: 'PR title; ": <package> <version>, ..." is appended'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
body:
|
||||||
|
description: 'PR body'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
PKGS_BOT_TOKEN:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pr:
|
||||||
|
if: ${{ inputs.groups != '[]' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 4
|
||||||
|
matrix:
|
||||||
|
group: ${{ fromJSON(inputs.groups) }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Fetch the pins
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: package-pins
|
||||||
|
path: ${{ runner.temp }}
|
||||||
|
|
||||||
|
# The PR title names what moved, so the merged history reads like a
|
||||||
|
# changelog: "Sync upstream releases: zed 1.23.2-1".
|
||||||
|
- name: Apply this PR's packages
|
||||||
|
id: apply
|
||||||
|
env:
|
||||||
|
PACKAGES: ${{ matrix.group.packages }}
|
||||||
|
TITLE: ${{ inputs.title }}
|
||||||
|
run: |
|
||||||
|
read -r -a packages <<<"$PACKAGES"
|
||||||
|
git apply $(printf -- '--include=pkgbuilds/%s/* ' "${packages[@]}") "$RUNNER_TEMP/pins.patch"
|
||||||
|
git status --porcelain
|
||||||
|
field() { sed -n "s/^$2=//p" "pkgbuilds/$1/PKGBUILD" | head -1 | tr -d "\"'"; }
|
||||||
|
versions=$(for package in "${packages[@]}"; do
|
||||||
|
printf '%s %s-%s, ' "$package" "$(field "$package" pkgver)" "$(field "$package" pkgrel)"
|
||||||
|
done)
|
||||||
|
title="$TITLE: ${versions%, }"
|
||||||
|
echo "title=$title" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "- $title" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
- name: Open or update the PR
|
||||||
|
id: pr
|
||||||
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||||
|
commit-message: ${{ steps.apply.outputs.title }}
|
||||||
|
title: ${{ steps.apply.outputs.title }}
|
||||||
|
body: ${{ inputs.body }}
|
||||||
|
branch: ${{ matrix.group.branch }}
|
||||||
|
delete-branch: true
|
||||||
|
labels: automated
|
||||||
|
|
||||||
|
# Auto-merge, not a direct merge: branch protection still has to see
|
||||||
|
# `result`, `self-tests` and `build-isolation` green. auto-merge-pr.yml
|
||||||
|
# may arm the same PR at the same moment; either one arming it is the
|
||||||
|
# outcome wanted.
|
||||||
|
- name: Enable auto-merge
|
||||||
|
if: ${{ steps.pr.outputs.pull-request-number != '' }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||||
|
PR: ${{ steps.pr.outputs.pull-request-number }}
|
||||||
|
run: |
|
||||||
|
armed() { [[ "$(gh pr view "$PR" -R "$GITHUB_REPOSITORY" --json autoMergeRequest --jq '.autoMergeRequest != null')" == true ]]; }
|
||||||
|
if armed; then
|
||||||
|
echo "auto-merge already enabled on #$PR"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
gh pr merge --auto --merge "$PR" -R "$GITHUB_REPOSITORY" || armed
|
||||||
|
|
||||||
|
# A PR on this lane's prefix that the run did not refresh, and that
|
||||||
|
# conflicts with master, is stale: master moved its packages on without it.
|
||||||
|
# Close it rather than leave a red PR behind. Not being refreshed is not
|
||||||
|
# enough on its own, since a feed that failed this run (one of dozens timing
|
||||||
|
# out is routine) leaves a still-valid PR unrefreshed too. The lane's old
|
||||||
|
# batch PR, on the bare prefix, is retired outright.
|
||||||
|
close-superseded:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Close superseded PRs
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||||
|
GROUPS_JSON: ${{ inputs.groups }}
|
||||||
|
PREFIX: ${{ inputs.prefix }}
|
||||||
|
run: |
|
||||||
|
gh pr list -R "$GITHUB_REPOSITORY" --state open --limit 500 --json number,headRefName,mergeable \
|
||||||
|
--jq '.[] | select(.headRefName == env.PREFIX or (.headRefName | startswith(env.PREFIX + "-"))) | "\(.number) \(.headRefName) \(.mergeable)"' |
|
||||||
|
while read -r number branch mergeable; do
|
||||||
|
jq -e --arg branch "$branch" 'any(.[]; .branch == $branch)' <<<"$GROUPS_JSON" >/dev/null && continue
|
||||||
|
[[ $branch == "$PREFIX" || $mergeable == CONFLICTING ]] || continue
|
||||||
|
echo "closing #$number ($branch): superseded"
|
||||||
|
gh pr close "$number" -R "$GITHUB_REPOSITORY" --delete-branch \
|
||||||
|
--comment "Superseded: master has moved these packages on, or per-package PRs carry them now."
|
||||||
|
done
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
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
|
||||||
|
# queue: max, because by default a group holds one waiting job: a third
|
||||||
|
# merge arriving cancels the second one's publish, and nothing reruns it.
|
||||||
|
# With it up to 100 wait, in the order they reach this job.
|
||||||
|
concurrency:
|
||||||
|
group: publish
|
||||||
|
cancel-in-progress: false
|
||||||
|
queue: max
|
||||||
|
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,16 @@
|
|||||||
name: Sync Rebuild Triggers
|
name: Sync Rebuild Triggers
|
||||||
|
|
||||||
|
# Rebuilds ride the unattended lane, like track-branches.yml: each package's
|
||||||
|
# pkgrel bump gets its own PR on auto/sync-rebuilds-<package>, opened by
|
||||||
|
# package-prs.yml. It 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
|
||||||
|
# without holding back any other package.
|
||||||
|
#
|
||||||
|
# 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:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
# Every 6 hours, off the hour to dodge the scheduling backlog at :00
|
# Every 6 hours, off the hour to dodge the scheduling backlog at :00
|
||||||
@@ -11,14 +22,30 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
|
|
||||||
|
# One sync at a time: two runs racing on one auto/sync-rebuilds-* branch
|
||||||
|
# would each force-push their own bump over the other's.
|
||||||
|
concurrency:
|
||||||
|
group: sync-rebuilds
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync:
|
sync:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
pull-requests: write
|
outputs:
|
||||||
|
groups: ${{ steps.groups.outputs.groups }}
|
||||||
|
|
||||||
steps:
|
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
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -30,6 +57,7 @@ jobs:
|
|||||||
# version the build never saw is the one failure this command must not
|
# version the build never saw is the one failure this command must not
|
||||||
# have: nothing re-fires once the record matches.
|
# have: nothing re-fires once the record matches.
|
||||||
- name: Bump pkgrel for packages whose dependencies moved
|
- name: Bump pkgrel for packages whose dependencies moved
|
||||||
|
id: sync
|
||||||
run: |
|
run: |
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-e PACKAGES="$PACKAGES" \
|
-e PACKAGES="$PACKAGES" \
|
||||||
@@ -39,7 +67,7 @@ jobs:
|
|||||||
-v "$PWD/helpers:/workspace/helpers:ro" \
|
-v "$PWD/helpers:/workspace/helpers:ro" \
|
||||||
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
|
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
|
||||||
-w /workspace \
|
-w /workspace \
|
||||||
archlinux:base-devel bash -lc '
|
ghcr.io/archlinux/archlinux:base-devel bash -lc '
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
printf "Server = https://mirror.omarchy.org/\$repo/os/\$arch\n" > /etc/pacman.d/mirrorlist
|
printf "Server = https://mirror.omarchy.org/\$repo/os/\$arch\n" > /etc/pacman.d/mirrorlist
|
||||||
@@ -59,38 +87,53 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
PACKAGES: ${{ github.event.inputs.packages }}
|
PACKAGES: ${{ github.event.inputs.packages }}
|
||||||
|
|
||||||
- name: Check for changes
|
- name: Group the bumps into PRs
|
||||||
id: changes
|
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }}
|
||||||
|
id: groups
|
||||||
run: |
|
run: |
|
||||||
if [ -z "$(git status --porcelain)" ]; then
|
groups=$(.github/scripts/package-pr-groups.sh auto/sync-rebuilds "$RUNNER_TEMP/pins.patch")
|
||||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
echo "groups=$groups" >> "$GITHUB_OUTPUT"
|
||||||
else
|
|
||||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Hand the bumps to the PR jobs
|
||||||
if: steps.changes.outputs.has_changes == 'true'
|
if: ${{ !cancelled() && steps.groups.outputs.groups != '' && steps.groups.outputs.groups != '[]' }}
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
name: package-pins
|
||||||
commit-message: 'chore: rebuild against updated dependencies'
|
path: ${{ runner.temp }}/pins.patch
|
||||||
title: 'chore: rebuild against updated dependencies'
|
retention-days: 1
|
||||||
body: |
|
|
||||||
Automated pkgrel bump for packages that link against a dependency
|
|
||||||
which has moved in the official repositories.
|
|
||||||
|
|
||||||
Each package names those dependencies in `rebuild_on` and carries the
|
prs:
|
||||||
versions its current pkgrel was bumped for in `rebuilt_against`. The
|
needs: sync
|
||||||
bump is what makes the rebuilt package an upgrade pacman will offer;
|
if: ${{ !cancelled() && needs.sync.outputs.groups != '' }}
|
||||||
without it the build produces the version already published and no
|
permissions:
|
||||||
one receives it.
|
contents: read
|
||||||
branch: auto/sync-rebuilds
|
uses: ./.github/workflows/package-prs.yml
|
||||||
delete-branch: true
|
with:
|
||||||
labels: automated
|
groups: ${{ needs.sync.outputs.groups }}
|
||||||
reviewers: ryanrhughes
|
prefix: auto/sync-rebuilds
|
||||||
|
title: Rebuild against updated dependencies
|
||||||
|
body: |
|
||||||
|
Automated pkgrel bump for a package that links against a dependency
|
||||||
|
which has moved in the official repositories.
|
||||||
|
|
||||||
|
Each package names those dependencies in `rebuild_on` and carries the
|
||||||
|
versions its current pkgrel was bumped for in `rebuilt_against`. The
|
||||||
|
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.
|
||||||
|
|
||||||
|
This PR auto-merges once the build checks pass. A failing rebuild
|
||||||
|
leaves it open for a maintainer without holding back any other package.
|
||||||
|
secrets:
|
||||||
|
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||||
|
|
||||||
|
notify:
|
||||||
|
needs: [sync, prs]
|
||||||
|
if: ${{ failure() }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
- name: Notify Basecamp on failure
|
- name: Notify Basecamp on failure
|
||||||
if: failure() && env.BASECAMP_CHATBOT_URL != ''
|
if: ${{ env.BASECAMP_CHATBOT_URL != '' }}
|
||||||
env:
|
env:
|
||||||
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
|
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
name: Sync Upstream Releases
|
name: Sync Upstream Releases
|
||||||
|
|
||||||
|
# Bumps every package whose upstream release feed has moved, except the
|
||||||
|
# "auto_merge": true packages that track-branches.yml checks hourly. Each
|
||||||
|
# package gets its own PR on auto/sync-upstream-<package>, opened by
|
||||||
|
# package-prs.yml: the PR builds on the droplets, auto-merge lands it when
|
||||||
|
# `result` is green, and the merge publishes. A package that fails to build
|
||||||
|
# stays an open red PR for a maintainer without holding back any other; the
|
||||||
|
# next run updates it if upstream moves again. `min_release_age` still
|
||||||
|
# quarantines fresh releases before they are proposed.
|
||||||
|
#
|
||||||
|
# The PRs and auto-merge use the PAT in PKGS_BOT_TOKEN, so their pushes
|
||||||
|
# build without an approval hold and their merges start publish.yml.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
# Every 6 hours, off the hour to dodge the scheduling backlog at :00
|
# Every 6 hours, off the hour to dodge the scheduling backlog at :00
|
||||||
@@ -11,14 +23,30 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
|
|
||||||
|
# One sync at a time: two runs racing on one auto/sync-upstream-* branch
|
||||||
|
# would each force-push their own update over the other's.
|
||||||
|
concurrency:
|
||||||
|
group: sync-upstream
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync:
|
sync:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
pull-requests: write
|
outputs:
|
||||||
|
groups: ${{ steps.groups.outputs.groups }}
|
||||||
|
|
||||||
steps:
|
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
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -27,63 +55,84 @@ jobs:
|
|||||||
# Runs in an Arch container for vercmp: whether a release is an upgrade has
|
# 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.
|
# to be decided by the same comparator pacman will use on users' machines.
|
||||||
- name: Update packages from upstream release feeds
|
- name: Update packages from upstream release feeds
|
||||||
|
id: sync
|
||||||
run: |
|
run: |
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-e PACKAGES="$PACKAGES" \
|
-e PACKAGES="$PACKAGES" \
|
||||||
|
-e UPSTREAM_GITHUB_TOKEN="$UPSTREAM_GITHUB_TOKEN" \
|
||||||
-e HOST_UID="$(id -u)" \
|
-e HOST_UID="$(id -u)" \
|
||||||
-e HOST_GID="$(id -g)" \
|
-e HOST_GID="$(id -g)" \
|
||||||
-v "$PWD/bin:/workspace/bin:ro" \
|
-v "$PWD/bin:/workspace/bin:ro" \
|
||||||
-v "$PWD/helpers:/workspace/helpers:ro" \
|
-v "$PWD/helpers:/workspace/helpers:ro" \
|
||||||
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
|
-v "$PWD/pkgbuilds:/workspace/pkgbuilds" \
|
||||||
-w /workspace \
|
-w /workspace \
|
||||||
archlinux:base-devel bash -lc '
|
ghcr.io/archlinux/archlinux:base-devel bash -lc '
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
pacman -Syu --noconfirm jq
|
pacman -Syu --noconfirm git jq python libarchive
|
||||||
|
|
||||||
groupadd -g "$HOST_GID" runner
|
groupadd -g "$HOST_GID" runner
|
||||||
useradd -m -u "$HOST_UID" -g "$HOST_GID" runner
|
useradd -m -u "$HOST_UID" -g "$HOST_GID" runner
|
||||||
chown -R runner:runner /workspace/pkgbuilds
|
chown -R runner:runner /workspace/pkgbuilds
|
||||||
|
|
||||||
|
# Packages marked auto_merge ride track-branches.yml instead,
|
||||||
|
# which checks them hourly.
|
||||||
if [[ -n "${PACKAGES:-}" ]]; then
|
if [[ -n "${PACKAGES:-}" ]]; then
|
||||||
read -r -a package_args <<< "$PACKAGES"
|
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
|
else
|
||||||
runuser -u runner -- ./bin/sync-upstream
|
runuser -u runner -- ./bin/sync-upstream --lane reviewed
|
||||||
fi
|
fi
|
||||||
'
|
'
|
||||||
env:
|
env:
|
||||||
PACKAGES: ${{ github.event.inputs.packages }}
|
PACKAGES: ${{ github.event.inputs.packages }}
|
||||||
|
UPSTREAM_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Check for changes
|
# Failed feeds leave their recipes untouched; completed updates still
|
||||||
id: changes
|
# get their PRs. The failed sync step keeps the workflow red and notifies.
|
||||||
|
- name: Group the updates into PRs
|
||||||
|
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }}
|
||||||
|
id: groups
|
||||||
run: |
|
run: |
|
||||||
if [ -z "$(git status --porcelain)" ]; then
|
groups=$(.github/scripts/package-pr-groups.sh auto/sync-upstream "$RUNNER_TEMP/pins.patch")
|
||||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
echo "groups=$groups" >> "$GITHUB_OUTPUT"
|
||||||
else
|
|
||||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Hand the updates to the PR jobs
|
||||||
if: steps.changes.outputs.has_changes == 'true'
|
if: ${{ !cancelled() && steps.groups.outputs.groups != '' && steps.groups.outputs.groups != '[]' }}
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
name: package-pins
|
||||||
commit-message: 'chore: sync upstream releases'
|
path: ${{ runner.temp }}/pins.patch
|
||||||
title: 'chore: sync upstream releases'
|
retention-days: 1
|
||||||
body: |
|
|
||||||
Automated update of packages that track an upstream vendor release
|
|
||||||
feed rather than the AUR.
|
|
||||||
|
|
||||||
Each package reports its newest release through
|
prs:
|
||||||
`.omarchy/upstream.sh`.
|
needs: sync
|
||||||
branch: auto/sync-upstream
|
if: ${{ !cancelled() && needs.sync.outputs.groups != '' }}
|
||||||
delete-branch: true
|
permissions:
|
||||||
labels: automated
|
contents: read
|
||||||
reviewers: ryanrhughes
|
uses: ./.github/workflows/package-prs.yml
|
||||||
|
with:
|
||||||
|
groups: ${{ needs.sync.outputs.groups }}
|
||||||
|
prefix: auto/sync-upstream
|
||||||
|
title: Sync upstream releases
|
||||||
|
body: |
|
||||||
|
Automated update of a package that tracks an upstream vendor release
|
||||||
|
feed rather than the AUR.
|
||||||
|
|
||||||
|
Release watches and providers are declared in `.omarchy/package.json`;
|
||||||
|
exceptional feeds use `.omarchy/upstream.sh`. This PR auto-merges once
|
||||||
|
the build checks pass. A failing build leaves it open for a maintainer
|
||||||
|
without holding back any other package.
|
||||||
|
secrets:
|
||||||
|
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||||
|
|
||||||
|
notify:
|
||||||
|
needs: [sync, prs]
|
||||||
|
if: ${{ failure() }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
- name: Notify Basecamp on failure
|
- name: Notify Basecamp on failure
|
||||||
if: failure() && env.BASECAMP_CHATBOT_URL != ''
|
if: ${{ env.BASECAMP_CHATBOT_URL != '' }}
|
||||||
env:
|
env:
|
||||||
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
|
BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,12 +1,28 @@
|
|||||||
name: Tests
|
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:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
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:
|
self-tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
@@ -16,6 +32,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
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
|
# An Arch container for vercmp: version ordering has to be decided by
|
||||||
# the same comparator pacman uses on users' machines.
|
# the same comparator pacman uses on users' machines.
|
||||||
- name: Run self-tests
|
- name: Run self-tests
|
||||||
@@ -23,10 +45,36 @@ jobs:
|
|||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$PWD:/workspace:ro" \
|
-v "$PWD:/workspace:ro" \
|
||||||
-w /workspace \
|
-w /workspace \
|
||||||
archlinux:base-devel bash -lc '
|
ghcr.io/archlinux/archlinux:base-devel bash -lc '
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
pacman -Syu --noconfirm jq
|
pacman -Syu --noconfirm git jq python libarchive neovim tmux
|
||||||
|
python tests/owe-package-restart.py
|
||||||
|
python tests/oma-service-removal.py
|
||||||
|
python tests/passwordless-package-lifecycle.py
|
||||||
|
python tests/upstream-watch.py
|
||||||
|
bash tests/ipu7-install.sh
|
||||||
|
bash tests/ipu7-headers.sh
|
||||||
./bin/sync-upstream self-test
|
./bin/sync-upstream self-test
|
||||||
|
./bin/sync-rebuilds --self-test
|
||||||
./bin/omarchy-pkgs self-test
|
./bin/omarchy-pkgs self-test
|
||||||
./bin/omarchy-release 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/passwordless-package-files.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
|
||||||
|
shopt -s nullglob
|
||||||
|
for test in pkgbuilds/*/test.sh; do
|
||||||
|
bash "$test"
|
||||||
|
done
|
||||||
'
|
'
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
name: Track upstream branches
|
||||||
|
|
||||||
|
# The hourly lane. Packages marked "auto_merge": true update here instead of
|
||||||
|
# in sync-upstream.yml's 6-hourly run: some 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; the rest are release feeds worth shipping within the
|
||||||
|
# hour (browsers, large vendors, Omacom's own projects). 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.
|
||||||
|
#
|
||||||
|
# One PR per package, on auto/track-branches-<package>, opened by
|
||||||
|
# package-prs.yml. Packages pinned from the same upstream branch
|
||||||
|
# (omarchy-dev and omarchy-settings-dev) share one, because sync-upstream
|
||||||
|
# moves them in lockstep.
|
||||||
|
#
|
||||||
|
# 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:
|
||||||
|
# Hourly, off the hour to dodge the scheduling backlog at :00. GitHub
|
||||||
|
# drops scheduled runs under load: the old every-2-hours entry fired
|
||||||
|
# every 6 to 9 hours in practice.
|
||||||
|
- cron: '35 * * * *'
|
||||||
|
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 one auto/track-branches-* branch
|
||||||
|
# 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
|
||||||
|
outputs:
|
||||||
|
groups: ${{ steps.groups.outputs.groups }}
|
||||||
|
|
||||||
|
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 sync-upstream.yml: 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 \
|
||||||
|
ghcr.io/archlinux/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: Group the pins into PRs
|
||||||
|
if: ${{ !cancelled() && steps.sync.outcome != 'skipped' }}
|
||||||
|
id: groups
|
||||||
|
run: |
|
||||||
|
groups=$(.github/scripts/package-pr-groups.sh auto/track-branches "$RUNNER_TEMP/pins.patch")
|
||||||
|
echo "groups=$groups" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Hand the pins to the PR jobs
|
||||||
|
if: ${{ !cancelled() && steps.groups.outputs.groups != '' && steps.groups.outputs.groups != '[]' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: package-pins
|
||||||
|
path: ${{ runner.temp }}/pins.patch
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
prs:
|
||||||
|
needs: track
|
||||||
|
if: ${{ !cancelled() && needs.track.outputs.groups != '' }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
uses: ./.github/workflows/package-prs.yml
|
||||||
|
with:
|
||||||
|
groups: ${{ needs.track.outputs.groups }}
|
||||||
|
prefix: auto/track-branches
|
||||||
|
title: Track upstream branches
|
||||||
|
body: |
|
||||||
|
Automated pin of a package that follows a moving upstream branch or a
|
||||||
|
trusted release feed (`"auto_merge": true` in `.omarchy/package.json`).
|
||||||
|
Its recipe now points at the newest upstream release, or the newest
|
||||||
|
branch tip whose commit timestamp is at least `min_release_age` old.
|
||||||
|
|
||||||
|
This PR auto-merges once the build checks pass. A failing build leaves
|
||||||
|
it open without holding back any other package; the next tracker run
|
||||||
|
replaces it with the newer tip.
|
||||||
|
secrets:
|
||||||
|
PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }}
|
||||||
|
|
||||||
|
notify:
|
||||||
|
needs: [track, prs]
|
||||||
|
if: ${{ failure() }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Notify Basecamp on failure
|
||||||
|
if: ${{ 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,81 @@
|
|||||||
|
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.
|
||||||
|
# queue: max, as there: a waiting job is queued, not replaced.
|
||||||
|
concurrency:
|
||||||
|
group: publish
|
||||||
|
cancel-in-progress: false
|
||||||
|
queue: max
|
||||||
|
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/
|
src/
|
||||||
logs/
|
logs/
|
||||||
build-output/
|
build-output/
|
||||||
|
cache/
|
||||||
pkgs.omarchy.org/
|
pkgs.omarchy.org/
|
||||||
pkgbuilds/*/.SRCINFO
|
pkgbuilds/*/.SRCINFO
|
||||||
pkgbuilds/*/.gitignore
|
pkgbuilds/*/.gitignore
|
||||||
@@ -35,3 +37,7 @@ pkgbuilds/yay/yay/
|
|||||||
.srcdest/
|
.srcdest/
|
||||||
.repo-host
|
.repo-host
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
# Python helpers and offline tests
|
||||||
|
__pycache__/
|
||||||
|
.release-verification/
|
||||||
+19
-20
@@ -6,7 +6,7 @@ source "$BUILD_ROOT/helpers/message-helpers.sh"
|
|||||||
source "$BUILD_ROOT/helpers/paths.sh"
|
source "$BUILD_ROOT/helpers/paths.sh"
|
||||||
|
|
||||||
PACKAGE=""
|
PACKAGE=""
|
||||||
SOURCE="aur"
|
SOURCE="local"
|
||||||
SYNC="true"
|
SYNC="true"
|
||||||
RELEASE_RING=""
|
RELEASE_RING=""
|
||||||
AUR_PACKAGE=""
|
AUR_PACKAGE=""
|
||||||
@@ -17,14 +17,14 @@ usage() {
|
|||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: $0 <package> [OPTIONS]
|
Usage: $0 <package> [OPTIONS]
|
||||||
|
|
||||||
Create pkgbuilds/<package> with Omarchy metadata. AUR packages are synced
|
Create an Omarchy-owned package. --source aur imports a starting recipe once;
|
||||||
immediately after metadata is written.
|
future releases must use an upstream watch, provider, or hook.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--source <aur|local> Package source (default: aur)
|
--source <aur|local> Initial recipe source (default: local)
|
||||||
--local Shortcut for --source local
|
--local Shortcut for --source local
|
||||||
--aur <name> AUR package name when different from local directory
|
--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
|
--fast Put package in the fast release ring
|
||||||
--release-ring <ring> Release ring (currently: fast)
|
--release-ring <ring> Release ring (currently: fast)
|
||||||
--scaffold For local packages, create a starter PKGBUILD
|
--scaffold For local packages, create a starter PKGBUILD
|
||||||
@@ -32,9 +32,9 @@ Options:
|
|||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
$0 yay
|
$0 yay --source aur
|
||||||
$0 spotify --fast
|
$0 spotify --source aur --fast
|
||||||
$0 signal-desktop --no-sync
|
$0 signal-desktop --source aur --no-sync
|
||||||
$0 omarchy-zsh --local --scaffold
|
$0 omarchy-zsh --local --scaffold
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -97,6 +97,10 @@ if [[ -z "$PACKAGE" ]]; then
|
|||||||
usage
|
usage
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if [[ ! $PACKAGE =~ ^[a-zA-Z0-9@_+][a-zA-Z0-9@._+-]*$ ]]; then
|
||||||
|
print_error "Invalid package name: $PACKAGE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
case "$SOURCE" in
|
case "$SOURCE" in
|
||||||
aur|local) ;;
|
aur|local) ;;
|
||||||
@@ -112,6 +116,11 @@ PACKAGE_DIR="$PKGBUILDS_DIR/$PACKAGE"
|
|||||||
OMARCHY_DIR="$PACKAGE_DIR/.omarchy"
|
OMARCHY_DIR="$PACKAGE_DIR/.omarchy"
|
||||||
METADATA_FILE="$OMARCHY_DIR/package.json"
|
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
|
if [[ -f "$METADATA_FILE" && "$FORCE" != true ]]; then
|
||||||
print_error "Package metadata already exists: $METADATA_FILE"
|
print_error "Package metadata already exists: $METADATA_FILE"
|
||||||
print_info "Use --force to overwrite it"
|
print_info "Use --force to overwrite it"
|
||||||
@@ -146,18 +155,8 @@ jq -n "${jq_args[@]}" "$jq_filter" > "$METADATA_FILE"
|
|||||||
print_success "Wrote $METADATA_FILE"
|
print_success "Wrote $METADATA_FILE"
|
||||||
|
|
||||||
if [[ "$SOURCE" == "aur" ]]; then
|
if [[ "$SOURCE" == "aur" ]]; then
|
||||||
if [[ "$SYNC" == "false" ]]; then
|
"$BUILD_ROOT/bin/import-aur" "$PACKAGE"
|
||||||
# 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
|
|
||||||
else
|
else
|
||||||
if [[ "$SCAFFOLD" == true && ! -f "$PACKAGE_DIR/PKGBUILD" ]]; then
|
if [[ "$SCAFFOLD" == true && ! -f "$PACKAGE_DIR/PKGBUILD" ]]; then
|
||||||
cat > "$PACKAGE_DIR/PKGBUILD" <<EOF
|
cat > "$PACKAGE_DIR/PKGBUILD" <<EOF
|
||||||
|
|||||||
+57
-10
@@ -25,6 +25,22 @@ SKIP_PROD_CHECK=false
|
|||||||
FAST_RING_ONLY=false
|
FAST_RING_ONLY=false
|
||||||
BOOTSTRAP=false
|
BOOTSTRAP=false
|
||||||
PACKAGES=""
|
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() {
|
usage() {
|
||||||
echo "Usage: $0 --from <channel> --to <channel> [OPTIONS]"
|
echo "Usage: $0 --from <channel> --to <channel> [OPTIONS]"
|
||||||
@@ -36,7 +52,8 @@ usage() {
|
|||||||
echo " or --bootstrap (one-time initial seed)"
|
echo " or --bootstrap (one-time initial seed)"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
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 " --package <names...> Advance only the named package(s)"
|
||||||
echo " --fast-ring Restrict to fast-ring packages (stable -> rc parity copy)"
|
echo " --fast-ring Restrict to fast-ring packages (stable -> rc parity copy)"
|
||||||
echo " --bootstrap One-time stable -> rc seed before forward-only enforcement"
|
echo " --bootstrap One-time stable -> rc seed before forward-only enforcement"
|
||||||
@@ -65,8 +82,14 @@ while [[ $# -gt 0 ]]; do
|
|||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
--arch)
|
--arch)
|
||||||
ARCH="$2"
|
if [[ "$2" == "all" ]]; then
|
||||||
update_arch_paths
|
ALL_ARCHES=true
|
||||||
|
ARCH=all
|
||||||
|
else
|
||||||
|
require_valid_arch "$2"
|
||||||
|
ARCH="$2"
|
||||||
|
update_arch_paths
|
||||||
|
fi
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
--package)
|
--package)
|
||||||
@@ -134,6 +157,31 @@ case "$FROM->$TO" in
|
|||||||
;;
|
;;
|
||||||
esac
|
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"
|
SOURCE_DIR="$REPO_ROOT/$FROM/$ARCH"
|
||||||
TARGET_DIR="$REPO_ROOT/$TO/$ARCH"
|
TARGET_DIR="$REPO_ROOT/$TO/$ARCH"
|
||||||
SOURCE_DB="$SOURCE_DIR/omarchy.db.tar.zst"
|
SOURCE_DB="$SOURCE_DIR/omarchy.db.tar.zst"
|
||||||
@@ -278,7 +326,7 @@ while IFS=$'\t' read -r name base filename; do
|
|||||||
done < <(read_manifest)
|
done < <(read_manifest)
|
||||||
|
|
||||||
echo ""
|
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
|
if [[ ${#MISSING_FILES[@]} -gt 0 ]]; then
|
||||||
print_error "${#MISSING_FILES[@]} package(s) named in the $FROM database are missing on disk:"
|
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
|
fi
|
||||||
|
|
||||||
if [[ ${#DIFFERING[@]} -gt 0 ]]; then
|
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[@]}"
|
printf ' %s\n' "${DIFFERING[@]}"
|
||||||
echo "Published filenames are never rewritten, and two artifacts fighting over"
|
echo "Published filenames are never rewritten, so the $TO copy stays authoritative."
|
||||||
echo "one name means something built twice from different inputs. Resolve it"
|
echo "A same-name collision means this version reached $TO through another lineage"
|
||||||
echo "deliberately: bump pkgrel and rebuild so the new artifact gets a new"
|
echo "(native build, bootstrap seed, or a same-version rebuild) and the package has"
|
||||||
echo "filename, or remove the source copy if the destination is correct."
|
echo "not moved in $FROM since; it advances under a new filename when it does."
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == true ]]; then
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
|||||||
+104
-57
@@ -1,6 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Run the release workflow for a channel when work is queued.
|
# 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:
|
# 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"
|
source "$BUILD_ROOT/helpers/lock-helpers.sh"
|
||||||
|
|
||||||
MIRROR="${1:-}"
|
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
|
# Backoff schedule: 10m, 20m, 40m, 80m, 160m, 320m, then hourly-ish forever
|
||||||
# (capped at 6h, the cadence this system ran at before frequent timers).
|
# (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}"
|
BACKOFF_MAX_SECONDS="${OMARCHY_BACKOFF_MAX:-21600}"
|
||||||
|
|
||||||
if [[ -z "$MIRROR" ]]; then
|
if [[ -z "$MIRROR" ]]; then
|
||||||
print_error "Usage: $0 <mirror>"
|
print_error "Usage: $0 <mirror> [<arch>|all]"
|
||||||
echo " mirror: edge, rc, or stable"
|
echo " mirror: edge, rc, or stable"
|
||||||
|
echo " arch: one of $VALID_ARCHES, or all (default) for every published architecture"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -39,17 +44,13 @@ if [[ "$MIRROR" != "edge" && "$MIRROR" != "rc" && "$MIRROR" != "stable" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
STATE_FILE="$STATE_DIR/.sync-needed-$MIRROR"
|
if [[ "$ARCH_ARG" == "all" ]]; then
|
||||||
FAIL_FILE="$STATE_DIR/.build-failed-$MIRROR"
|
ARCHES=$(published_arches)
|
||||||
|
else
|
||||||
# Nothing queued: stay quiet. At a 5-minute cadence this is most invocations,
|
require_valid_arch "$ARCH_ARG"
|
||||||
# and a header for each would bury the runs that matter in the journal.
|
ARCHES="$ARCH_ARG"
|
||||||
if [[ ! -f "$STATE_FILE" ]]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
print_header "Processing Sync for $MIRROR"
|
|
||||||
|
|
||||||
# The build inputs are this checkout's contents; its HEAD identifies them.
|
# The build inputs are this checkout's contents; its HEAD identifies them.
|
||||||
current_fingerprint() {
|
current_fingerprint() {
|
||||||
git -C "$BUILD_ROOT" rev-parse HEAD 2>/dev/null || echo "unknown"
|
git -C "$BUILD_ROOT" rev-parse HEAD 2>/dev/null || echo "unknown"
|
||||||
@@ -65,57 +66,103 @@ backoff_seconds() {
|
|||||||
echo "$delay"
|
echo "$delay"
|
||||||
}
|
}
|
||||||
|
|
||||||
FAIL_COUNT=0
|
# Returns 0 when this architecture's queue was processed (or was empty), 1 when
|
||||||
if [[ -f "$FAIL_FILE" ]]; then
|
# the release failed. Backoff and "someone else holds the lock" are not
|
||||||
# shellcheck disable=SC1090
|
# failures: they are this tick deciding to do nothing.
|
||||||
source "$FAIL_FILE" 2>/dev/null || true
|
release_arch() {
|
||||||
FAIL_COUNT="${FAILURE_COUNT:-0}"
|
local arch="$1"
|
||||||
failed_at="${FAILURE_AT:-0}"
|
local state_file fail_file
|
||||||
failed_fingerprint="${FAILURE_FINGERPRINT:-}"
|
state_file=$(sync_queue_file "$MIRROR" "$arch")
|
||||||
|
fail_file=$(sync_fail_file "$MIRROR" "$arch")
|
||||||
|
|
||||||
if [[ "$failed_fingerprint" != "$(current_fingerprint)" ]]; then
|
# A queue written under the pre-architecture name belongs to x86_64.
|
||||||
print_info "Repository changed since the last failure — clearing backoff and retrying"
|
if [[ "$arch" == "x86_64" && ! -f "$state_file" && -f "$(legacy_sync_queue_file "$MIRROR")" ]]; then
|
||||||
rm -f "$FAIL_FILE"
|
state_file=$(legacy_sync_queue_file "$MIRROR")
|
||||||
FAIL_COUNT=0
|
fi
|
||||||
else
|
if [[ "$arch" == "x86_64" && ! -f "$fail_file" && -f "$(legacy_sync_fail_file "$MIRROR")" ]]; then
|
||||||
delay=$(backoff_seconds "$FAIL_COUNT")
|
fail_file=$(legacy_sync_fail_file "$MIRROR")
|
||||||
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)"
|
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
# Non-blocking: a build in progress means this tick has nothing to do.
|
# Nothing queued: stay quiet. At a 5-minute cadence this is most
|
||||||
if ! try_release_lock; then
|
# invocations, and a header for each would bury the runs that matter in
|
||||||
holder=$(release_lock_holder)
|
# the journal.
|
||||||
print_info "A release is already running (${holder:-holder unknown}) — skipping this tick"
|
[[ -f "$state_file" ]] || return 0
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_info "State file found: $STATE_FILE"
|
print_header "Processing Sync for $MIRROR ($arch)"
|
||||||
print_info "Starting release workflow for $MIRROR..."
|
|
||||||
|
|
||||||
if "$BUILD_ROOT/bin/repo" release --mirror "$MIRROR" --skip-prod-check; then
|
local fail_count=0 failed_at failed_fingerprint delay now retry_at
|
||||||
print_success "Release completed successfully for $MIRROR"
|
if [[ -f "$fail_file" ]]; then
|
||||||
rm -f "$STATE_FILE"
|
FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT=""
|
||||||
rm -f "$FAIL_FILE"
|
# shellcheck disable=SC1090
|
||||||
print_success "State file removed: $STATE_FILE"
|
source "$fail_file" 2>/dev/null || true
|
||||||
else
|
fail_count="${FAILURE_COUNT:-0}"
|
||||||
status=$?
|
failed_at="${FAILURE_AT:-0}"
|
||||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
failed_fingerprint="${FAILURE_FINGERPRINT:-}"
|
||||||
cat >"$FAIL_FILE" <<EOF
|
|
||||||
FAILURE_COUNT=$FAIL_COUNT
|
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_AT=$(date +%s)
|
||||||
FAILURE_FINGERPRINT=$(current_fingerprint)
|
FAILURE_FINGERPRINT=$(current_fingerprint)
|
||||||
EOF
|
EOF
|
||||||
next=$(backoff_seconds "$FAIL_COUNT")
|
then
|
||||||
print_error "Release failed for $MIRROR (attempt $FAIL_COUNT)"
|
print_error "Could not record failure state: $fail_file"
|
||||||
print_warning "State file retained for retry: $STATE_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"
|
print_warning "Backing off $((next / 60))m before the next attempt; a new commit retries sooner"
|
||||||
exit "$status"
|
return 1
|
||||||
fi
|
}
|
||||||
|
|
||||||
|
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
|
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
|
# Parse command line arguments
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
@@ -65,7 +86,14 @@ while [[ $# -gt 0 ]]; do
|
|||||||
echo " $0 --arch aarch64"
|
echo " $0 --arch aarch64"
|
||||||
echo " $0 --mirror stable"
|
echo " $0 --mirror stable"
|
||||||
echo " $0 --package yay"
|
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 ""
|
echo ""
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
@@ -76,18 +104,55 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
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
|
# Show target architecture and mirror after parsing args
|
||||||
print_info "Target architecture: $ARCH"
|
print_info "Target architecture: $ARCH"
|
||||||
print_info "Mirror: $MIRROR"
|
print_info "Mirror: $MIRROR"
|
||||||
print_info "Build workspace: $BUILD_OUTPUT_DIR"
|
print_info "Build workspace: $BUILD_OUTPUT_DIR"
|
||||||
print_info "Final output: $REPO_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
|
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" \
|
ARCH="$ARCH" \
|
||||||
MIRROR="$MIRROR" \
|
MIRROR="$MIRROR" \
|
||||||
PACKAGES="$PACKAGES" \
|
PACKAGES="$PACKAGES" \
|
||||||
DRY_RUN=true \
|
DRY_RUN=true \
|
||||||
|
DEFER_RUNTIME_DEPS="$DEFER_RUNTIME_DEPS" \
|
||||||
PKGBUILDS_DIR="$PKGBUILDS_DIR" \
|
PKGBUILDS_DIR="$PKGBUILDS_DIR" \
|
||||||
BUILD_OUTPUT_DIR="$BUILD_OUTPUT_DIR" \
|
BUILD_OUTPUT_DIR="$BUILD_OUTPUT_DIR" \
|
||||||
FINAL_OUTPUT_DIR="$REPO_DIR" \
|
FINAL_OUTPUT_DIR="$REPO_DIR" \
|
||||||
@@ -100,21 +165,45 @@ fi
|
|||||||
# Create directories if they don't exist
|
# Create directories if they don't exist
|
||||||
mkdir -p "$BUILD_OUTPUT_DIR" "$REPO_DIR" "$SRC_DIR"
|
mkdir -p "$BUILD_OUTPUT_DIR" "$REPO_DIR" "$SRC_DIR"
|
||||||
|
|
||||||
# Check Docker is available
|
# Check the selected container engine is available
|
||||||
check_docker
|
check_engine
|
||||||
|
|
||||||
# Setup QEMU for aarch64 builds on x86_64 hosts
|
# A foreign target architecture runs under QEMU user emulation. Probe by
|
||||||
if [[ "$(uname -m)" == "x86_64" && "$ARCH" == "aarch64" ]]; then
|
# actually running a container for the target platform: that is the only
|
||||||
# Check if QEMU is already working
|
# test that covers both "binfmt not registered" and "registered but broken".
|
||||||
if ! docker run --rm --platform linux/arm64 alpine:3.21 /bin/true >/dev/null 2>&1; then
|
HOST_ARCH=$(uname -m)
|
||||||
print_info "Setting up QEMU for ARM64 emulation..."
|
[[ "$HOST_ARCH" == "arm64" ]] && HOST_ARCH=aarch64
|
||||||
setup_qemu
|
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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean build-output directory to start fresh
|
# Clean build-output directory to start fresh, unless the caller seeded it
|
||||||
print_info "Cleaning build workspace..."
|
# with packages from an earlier job or is resuming an interrupted run.
|
||||||
rm -rf "$BUILD_OUTPUT_DIR"/*
|
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"
|
mkdir -p "$BUILD_OUTPUT_DIR"
|
||||||
|
|
||||||
# Show package info
|
# Show package info
|
||||||
@@ -124,18 +213,43 @@ else
|
|||||||
print_info "Building unscoped packages for $MIRROR mirror"
|
print_info "Building unscoped packages for $MIRROR mirror"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build/update the Docker image
|
# Build/update the Docker image, unless the caller prepared the exact image
|
||||||
build_docker_image "$BUILD_DIR" "$ARCH" "$MIRROR"
|
# 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
|
# Create output directories if they don't exist
|
||||||
mkdir -p "$BUILD_OUTPUT_DIR"
|
mkdir -p "$BUILD_OUTPUT_DIR"
|
||||||
mkdir -p "$REPO_DIR"
|
mkdir -p "$REPO_DIR"
|
||||||
|
|
||||||
# Ensure output directories are writable by container user
|
# Share downloaded archives, never /var/lib/pacman or an installed root.
|
||||||
make_dir_writable "$BUILD_OUTPUT_DIR"
|
# Channel/architecture separation preserves each mirror's dependency set.
|
||||||
make_dir_writable "$REPO_DIR"
|
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
|
# Build Docker arguments
|
||||||
DOCKER_ARGS=(
|
DOCKER_ARGS=(
|
||||||
@@ -144,26 +258,101 @@ DOCKER_ARGS=(
|
|||||||
-e MIRROR="$MIRROR"
|
-e MIRROR="$MIRROR"
|
||||||
-e PACKAGES="$PACKAGES"
|
-e PACKAGES="$PACKAGES"
|
||||||
-e OMARCHY_RC_PINS="${OMARCHY_RC_PINS:-}"
|
-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 "$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_DIR:/build:ro"
|
||||||
-v "$BUILD_ROOT/helpers:/helpers:ro"
|
-v "$BUILD_ROOT/helpers:/helpers:ro"
|
||||||
-v "$BUILD_ROOT/pkgbuilds:/pkgbuilds:ro"
|
-v "$BUILD_ROOT/pkgbuilds:/pkgbuilds:ro"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run the builder with assembled args
|
# Podman-created images can leave WORKDIR owned by a remapped uid. Mount the
|
||||||
IMAGE_TAG="omarchy-pkg-builder:latest-$ARCH-$MIRROR"
|
# 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")
|
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 ""
|
echo ""
|
||||||
if [[ $BUILD_RESULT -eq 0 ]]; then
|
print_header "Build Summary"
|
||||||
print_success "Build completed successfully!"
|
echo " Total packages: ${#ORDERED_PACKAGES[@]}"
|
||||||
else
|
echo " Built: ${#SUCCESSFUL_PACKAGES[@]}"
|
||||||
print_warning "Some packages failed (see details above)"
|
echo " Skipped: ${#SKIPPED_PACKAGES[@]} (up-to-date or excluded)"
|
||||||
exit $BUILD_RESULT
|
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
|
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
|
#!/bin/bash
|
||||||
# Check PKGBUILD versions against published repo versions
|
# 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
|
set -e
|
||||||
|
|
||||||
@@ -10,11 +14,27 @@ source "$BUILD_ROOT/helpers/paths.sh"
|
|||||||
source "$BUILD_ROOT/helpers/package-metadata.sh"
|
source "$BUILD_ROOT/helpers/package-metadata.sh"
|
||||||
source "$BUILD_ROOT/helpers/lock-helpers.sh"
|
source "$BUILD_ROOT/helpers/lock-helpers.sh"
|
||||||
|
|
||||||
STATE_DIR="${OMARCHY_STATE_DIR:-/root/.state}"
|
|
||||||
ARCH="${ARCH:-x86_64}"
|
|
||||||
PULL=false
|
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"
|
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() {
|
get_pkgbuild_version() {
|
||||||
local pkgdir="$1"
|
local pkgdir="$1"
|
||||||
if [[ -f "$pkgdir/PKGBUILD" ]]; then
|
local epoch pkgver pkgrel
|
||||||
(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; if [[ -n "$epoch" ]]; then echo "${epoch}:${pkgver}-${pkgrel}"; else echo "${pkgver}-${pkgrel}"; fi')
|
|
||||||
|
[[ -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
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,8 +113,8 @@ check_vcs_unchanged() {
|
|||||||
|
|
||||||
# If epoch or pkgrel changed in PKGBUILD, release even if upstream is unchanged.
|
# If epoch or pkgrel changed in PKGBUILD, release even if upstream is unchanged.
|
||||||
local pkgbuild_epoch pkgbuild_pkgrel
|
local pkgbuild_epoch pkgbuild_pkgrel
|
||||||
pkgbuild_epoch=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${epoch:-}"')
|
pkgbuild_epoch=$(package_pkgbuild_var "$pkgdir" epoch "$ARCH")
|
||||||
pkgbuild_pkgrel=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${pkgrel}"')
|
pkgbuild_pkgrel=$(package_pkgbuild_var "$pkgdir" pkgrel "$ARCH")
|
||||||
|
|
||||||
local repo_pkgrel="${repo_version##*-}"
|
local repo_pkgrel="${repo_version##*-}"
|
||||||
local repo_no_pkgrel="${repo_version%-*}"
|
local repo_no_pkgrel="${repo_version%-*}"
|
||||||
@@ -106,12 +142,11 @@ check_package() {
|
|||||||
local mirror="$3"
|
local mirror="$3"
|
||||||
|
|
||||||
local pkgbuild_version repo_version
|
local pkgbuild_version repo_version
|
||||||
pkgbuild_version=$(get_pkgbuild_version "$pkgdir")
|
if ! pkgbuild_version=$(get_pkgbuild_version "$pkgdir") || [[ -z "$pkgbuild_version" ]]; then
|
||||||
repo_version=$(get_repo_version "$pkg" "$mirror")
|
print_warning "$pkg: could not read pkgver/pkgrel from its PKGBUILD for $ARCH — not queueing"
|
||||||
|
|
||||||
if [[ -z "$pkgbuild_version" ]]; then
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
repo_version=$(get_repo_version "$pkg" "$mirror")
|
||||||
|
|
||||||
if grep -qE '^pkgver[[:space:]]*\(\)' "$pkgdir/PKGBUILD"; then
|
if grep -qE '^pkgver[[:space:]]*\(\)' "$pkgdir/PKGBUILD"; then
|
||||||
if [[ -n "$repo_version" && -n "$(package_extract_vcs_hash_from_version "$repo_version")" ]]; 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.
|
# it because that exact filename is already published with different bytes.
|
||||||
# If the artifact for this version already exists in the channel, there is
|
# If the artifact for this version already exists in the channel, there is
|
||||||
# nothing to build regardless of which direction the versions differ.
|
# 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
|
if package_version_is_published "$REPO_ROOT/$mirror/$ARCH" "$pkg" "$pkgbuild_version" "$ARCH"; then
|
||||||
print_warning "$pkg $pkgbuild_version is already published — this checkout is behind the channel; not queueing"
|
print_warning "$pkg $pkgbuild_version is already published; not queueing"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -147,11 +182,12 @@ check_package() {
|
|||||||
|
|
||||||
check_mirror() {
|
check_mirror() {
|
||||||
local mirror="$1"
|
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 needs_build=false
|
||||||
local packages=()
|
local packages=()
|
||||||
|
|
||||||
print_info "Checking $mirror packages..."
|
print_info "Checking $mirror packages for $ARCH..."
|
||||||
|
|
||||||
while IFS= read -r pkg; do
|
while IFS= read -r pkg; do
|
||||||
local pkgdir="$PKGBUILDS_DIR/$pkg"
|
local pkgdir="$PKGBUILDS_DIR/$pkg"
|
||||||
@@ -159,7 +195,7 @@ check_mirror() {
|
|||||||
needs_build=true
|
needs_build=true
|
||||||
packages+=("$pkg")
|
packages+=("$pkg")
|
||||||
fi
|
fi
|
||||||
done < <(packages_for_unscoped_build "$mirror")
|
done < <(packages_for_unscoped_build "$mirror" "$ARCH")
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
@@ -168,18 +204,23 @@ check_mirror() {
|
|||||||
# this to name the packages in its start report, which is the difference
|
# 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".
|
# between "a build is running" and "your package is in this build".
|
||||||
printf '%s\n' "${packages[@]}" >"$state_file"
|
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 "Packages: ${packages[*]}"
|
||||||
print_info "State file created: $state_file"
|
print_info "State file created: $state_file"
|
||||||
else
|
else
|
||||||
print_info "${mirror^} is up to date"
|
print_info "${mirror^} ($ARCH) is up to date"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
check_mirror edge
|
# One pass per published architecture: the version comparison reads that
|
||||||
check_mirror rc
|
# architecture's channel databases, and each queue is its own file.
|
||||||
check_mirror stable
|
for ARCH in $ARCHES; do
|
||||||
|
update_arch_paths
|
||||||
|
check_mirror edge
|
||||||
|
check_mirror rc
|
||||||
|
check_mirror stable
|
||||||
|
done
|
||||||
|
|
||||||
print_success "Version check complete!"
|
print_success "Version check complete!"
|
||||||
+13
-7
@@ -1,21 +1,27 @@
|
|||||||
#!/bin/bash
|
#!/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
|
# Forces a fresh image build on next run
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
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
|
# Remove all omarchy-pkg-builder images
|
||||||
print_info "Removing 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
|
# Clear this engine's build cache.
|
||||||
print_info "Clearing buildx cache..."
|
print_info "Clearing build cache..."
|
||||||
docker buildx prune -a -f
|
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"
|
print_info "Next build will create fresh images"
|
||||||
+7
-3
@@ -31,9 +31,13 @@ clean_packages() {
|
|||||||
# Skip signature files
|
# Skip signature files
|
||||||
[[ "$pkg" == *.sig ]] && continue
|
[[ "$pkg" == *.sig ]] && continue
|
||||||
|
|
||||||
# Extract package name (remove version and architecture)
|
# Format: name-version-release-arch.pkg.tar.*. Work from the right because
|
||||||
# Format: name-version-release-arch.pkg.tar.*
|
# package names can themselves contain version-like pieces (qt6-5compat,
|
||||||
local pkgname=$(echo "$pkg" | sed -E 's/-[0-9]+.*-(any|x86_64|i686)\.pkg\.tar\..*//')
|
# 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
|
# Add to array
|
||||||
if [[ -n "${packages[$pkgname]}" ]]; then
|
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/message-helpers.sh"
|
||||||
source "$BUILD_ROOT/helpers/paths.sh"
|
source "$BUILD_ROOT/helpers/paths.sh"
|
||||||
source "$BUILD_ROOT/helpers/host-helpers.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}"
|
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)
|
RELEASE_PACKAGES=(omarchy omarchy-settings)
|
||||||
DEFAULT_RC_REF="quattro"
|
DEFAULT_RC_REF="quattro"
|
||||||
SRCDEST_DIR="$BUILD_ROOT/.srcdest"
|
SRCDEST_DIR="$BUILD_ROOT/.srcdest"
|
||||||
@@ -92,7 +93,7 @@ version_is_rc() { version_is_prerelease "$1"; }
|
|||||||
|
|
||||||
pkgbuild_var() {
|
pkgbuild_var() {
|
||||||
local pkg="$1" var="$2"
|
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
|
# Prints the published version of $pkg from the edge DB. Distinguishes
|
||||||
@@ -327,13 +328,19 @@ regenerate_checksums() {
|
|||||||
|
|
||||||
trigger_build_host() {
|
trigger_build_host() {
|
||||||
local 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
|
# Explicit host configuration outranks the local-host inference (a
|
||||||
# workstation that ran a full local release carries the db marker too).
|
# 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
|
if ! resolve_repo_host "${REPO_HOST_OVERRIDE:-}" >/dev/null && on_repo_host; then
|
||||||
print_info "Triggering edge build locally (this is the build host)..."
|
print_info "Triggering edge build locally (this is the build host)..."
|
||||||
if mkdir -p "${OMARCHY_STATE_DIR:-/root/.state}" &&
|
if queue_edge_builds && systemctl start --no-block omarchy-auto-release-edge.service; then
|
||||||
touch "${OMARCHY_STATE_DIR:-/root/.state}/.sync-needed-edge" &&
|
|
||||||
systemctl start --no-block omarchy-auto-release-edge.service; then
|
|
||||||
print_success "Edge build triggered"
|
print_success "Edge build triggered"
|
||||||
else
|
else
|
||||||
print_warning "Could not start the edge release service — the 6-hourly timer will pick it up"
|
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
|
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 "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:"
|
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
|
return 0
|
||||||
fi
|
fi
|
||||||
print_info "Triggering edge build on $host..."
|
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"
|
print_success "Edge build triggered on $host"
|
||||||
else
|
else
|
||||||
print_warning "Could not trigger $host — the 6-hourly timer will pick it up"
|
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]%/*}/..")
|
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||||
|
source "$BUILD_ROOT/helpers/docker-helpers.sh"
|
||||||
source "$BUILD_ROOT/helpers/paths.sh"
|
source "$BUILD_ROOT/helpers/paths.sh"
|
||||||
source "$BUILD_ROOT/helpers/host-helpers.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}"
|
DEV_BRANCH="${OMARCHY_DEV_BRANCH:-quattro}"
|
||||||
|
|
||||||
PKGS_DB_BASE="${OMARCHY_PKGS_DB_BASE:-https://pkgs.omarchy.org}"
|
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"
|
SRCDEST_DIR="$BUILD_ROOT/.srcdest"
|
||||||
MIRROR_CLONE="$SRCDEST_DIR/omarchy" # bare mirror (shared with bin/omarchy-pkgs)
|
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
|
# 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).
|
# the database cannot be read (callers must not mistake an outage for absence).
|
||||||
published_version() {
|
published_version() {
|
||||||
local channel="$1" tmp descs
|
local channel="$1" arch="${2:-$OBSERVED_ARCH}" tmp descs
|
||||||
tmp=$(mktemp) || return 2
|
tmp=$(mktemp) || return 2
|
||||||
# A unique query string busts the CDN cache: right after a sync the plain
|
# 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
|
# 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.
|
# 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"
|
rm -f "$tmp"
|
||||||
return 2
|
return 2
|
||||||
fi
|
fi
|
||||||
@@ -207,6 +212,14 @@ published_version() {
|
|||||||
' <<<"$descs"
|
' <<<"$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
|
# The rc branch of THIS repo carries the current pins. Read them without
|
||||||
# touching the working tree.
|
# touching the working tree.
|
||||||
rc_branch_pin() { # prints "pkgver commit", empty when no rc branch
|
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 fetch origin rc
|
||||||
git -C /root/omarchy-pkgs-rc reset --hard origin/rc
|
git -C /root/omarchy-pkgs-rc reset --hard origin/rc
|
||||||
cd /root/omarchy-pkgs-rc
|
cd /root/omarchy-pkgs-rc
|
||||||
OMARCHY_RC_PINS=1 OMARCHY_REPO_ROOT=/root/omarchy-pkgs/pkgs.omarchy.org \
|
# The pair is built once per published architecture (helpers/paths.sh on the
|
||||||
bin/repo release --mirror rc --package omarchy omarchy-settings --skip-prod-check
|
# 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() {
|
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.
|
# against this machine's (likely stale) local tree would be wrong.
|
||||||
if ! resolve_repo_host "$REPO_HOST_OVERRIDE" >/dev/null && ! on_repo_host; then
|
if ! resolve_repo_host "$REPO_HOST_OVERRIDE" >/dev/null && ! on_repo_host; then
|
||||||
print_no_host_help "advance $from -> $to" \
|
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
|
return 1
|
||||||
fi
|
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]
|
wait_for_published() { # wait_for_published <channel> <version> [timeout-seconds]
|
||||||
local channel="$1" want="$2" timeout="${3:-3600}" waited=0 got
|
local channel="$1" want="$2" timeout="${3:-3600}" waited=0 arch got pending
|
||||||
print_info "Waiting for $want to appear in the $channel channel (up to $((timeout / 60))m)..."
|
print_info "Waiting for $want in $channel for: $(published_arches | tr '\n' ' ')"
|
||||||
while ((waited < timeout)); do
|
while ((waited < timeout)); do
|
||||||
got=$(published_version "$channel" 2>/dev/null) || got=""
|
pending=""
|
||||||
if [[ "${got%-*}" == "$want" ]]; then
|
for arch in $(published_arches); do
|
||||||
print_success "$channel now serves omarchy $got"
|
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
|
return 0
|
||||||
fi
|
fi
|
||||||
sleep 60
|
sleep 60
|
||||||
@@ -298,7 +321,7 @@ wait_for_published() { # wait_for_published <channel> <version> [timeout-seconds
|
|||||||
printf '.' >&2
|
printf '.' >&2
|
||||||
done
|
done
|
||||||
echo "" >&2
|
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."
|
print_info "The build may still be running — re-run this command to resume."
|
||||||
return 1
|
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]
|
build_iso() { # build_iso <version> [--rc]
|
||||||
local version="$1" rc_flag="${2:-}" dir
|
local version="$1" rc_flag="${2:-}" dir
|
||||||
dir=$(iso_checkout) || return 1
|
dir=$(iso_checkout) || return 1
|
||||||
if ! command -v docker >/dev/null || ! docker info >/dev/null 2>&1; then
|
if [[ -z "$CONTAINER_ENGINE" ]] || ! "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
|
||||||
print_warning "Docker unavailable — cannot build the ISO here. Run on a Docker machine:"
|
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"
|
echo " cd $dir && bin/omarchy-iso-release ${rc_flag:+$rc_flag }$version"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -662,15 +685,13 @@ cmd_rc() {
|
|||||||
if [[ -n "$pin" ]]; then
|
if [[ -n "$pin" ]]; then
|
||||||
local pin_ver="${pin%% *}" pin_commit="${pin##* }"
|
local pin_ver="${pin%% *}" pin_commit="${pin##* }"
|
||||||
if [[ "$pin_commit" == "$head" && "$pin_ver" =~ ^${version//./\\.}rc[0-9]+$ ]]; then
|
if [[ "$pin_commit" == "$head" && "$pin_ver" =~ ^${version//./\\.}rc[0-9]+$ ]]; then
|
||||||
local pub
|
if all_arches_at_version rc "$pin_ver"; then
|
||||||
pub=$(published_version rc 2>/dev/null) || pub=""
|
print_success "$pin_ver is already cut from this head and published to rc on every architecture"
|
||||||
if [[ "${pub%-*}" == "$pin_ver" ]]; then
|
|
||||||
print_success "$pin_ver is already cut from this head and published to rc"
|
|
||||||
maybe_iso "$pin_ver" rc "$iso_mode"
|
maybe_iso "$pin_ver" rc "$iso_mode"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
print_info "$pin_ver is pinned from this head but not published yet — re-triggering the build"
|
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"
|
[[ "$wait" == true ]] && wait_for_published rc "$pin_ver"
|
||||||
maybe_iso "$pin_ver" rc "$iso_mode"
|
maybe_iso "$pin_ver" rc "$iso_mode"
|
||||||
return 0
|
return 0
|
||||||
@@ -686,7 +707,7 @@ cmd_rc() {
|
|||||||
new_ver="${new_pin%% *}"
|
new_ver="${new_pin%% *}"
|
||||||
print_success "Pinned $new_ver (rc branch pushed)"
|
print_success "Pinned $new_ver (rc branch pushed)"
|
||||||
|
|
||||||
trigger_rc_build || true
|
trigger_rc_build || return 1
|
||||||
if [[ "$wait" == true ]]; then
|
if [[ "$wait" == true ]]; then
|
||||||
wait_for_published rc "$new_ver" || return 1
|
wait_for_published rc "$new_ver" || return 1
|
||||||
fi
|
fi
|
||||||
@@ -708,9 +729,7 @@ cmd_ship() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
version=$(branch_to_version "$branch")
|
version=$(branch_to_version "$branch")
|
||||||
local stable_now
|
if all_arches_at_version stable "$version" &&
|
||||||
stable_now=$(published_version stable 2>/dev/null) || stable_now=""
|
|
||||||
if [[ "${stable_now%-*}" == "$version" ]] &&
|
|
||||||
gh release view "v$version" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; then
|
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"
|
print_success "Nothing to ship — $version is tagged, released, and live on stable"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -752,10 +771,8 @@ cmd_ship() {
|
|||||||
echo " omarchy-release rc"
|
echo " omarchy-release rc"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
local pub
|
if ! all_arches_at_version rc "$pin_ver"; then
|
||||||
pub=$(published_version rc 2>/dev/null) || pub=""
|
print_error "$pin_ver is pinned but is not published for every architecture"
|
||||||
if [[ "${pub%-*}" != "$pin_ver" ]]; then
|
|
||||||
print_error "$pin_ver is pinned but rc serves '${pub:-nothing}' — the candidate build hasn't published"
|
|
||||||
echo "Wait for it (or re-run: omarchy-release rc), then ship."
|
echo "Wait for it (or re-run: omarchy-release rc), then ship."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -799,10 +816,9 @@ cmd_ship() {
|
|||||||
|
|
||||||
# 2. Final pins into rc. Resolve the tag we just established so the final
|
# 2. Final pins into rc. Resolve the tag we just established so the final
|
||||||
# PKGBUILDs record both its provenance and its exact commit.
|
# PKGBUILDs record both its provenance and its exact commit.
|
||||||
local rc_pub stable_pub
|
local stable_pub
|
||||||
rc_pub=$(published_version rc 2>/dev/null) || rc_pub=""
|
if all_arches_at_version rc "$version"; then
|
||||||
if [[ "${rc_pub%-*}" == "$version" ]]; then
|
print_success "2/7 Final $version already published to rc on every architecture"
|
||||||
print_success "2/7 Final $version already published to rc"
|
|
||||||
else
|
else
|
||||||
if [[ "$pin_ver" != "$version" ]]; then
|
if [[ "$pin_ver" != "$version" ]]; then
|
||||||
print_info "2/7 Pinning final $version from tag v$version..."
|
print_info "2/7 Pinning final $version from tag v$version..."
|
||||||
@@ -810,22 +826,21 @@ cmd_ship() {
|
|||||||
else
|
else
|
||||||
print_info "2/7 Final $version pinned — re-triggering build"
|
print_info "2/7 Final $version pinned — re-triggering build"
|
||||||
fi
|
fi
|
||||||
trigger_rc_build || true
|
trigger_rc_build || exit 1
|
||||||
wait_for_published rc "$version" || exit 1
|
wait_for_published rc "$version" || exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. Promote rc -> stable
|
# 3. Promote rc -> stable
|
||||||
stable_pub=$(published_version stable 2>/dev/null) || stable_pub=""
|
if all_arches_at_version stable "$version"; then
|
||||||
if [[ "${stable_pub%-*}" == "$version" ]]; then
|
print_success "3/7 Stable already serves $version on every architecture"
|
||||||
print_success "3/7 Stable already serves $version"
|
|
||||||
else
|
else
|
||||||
host_advance rc stable || exit 1
|
host_advance rc stable || exit 1
|
||||||
stable_pub=$(published_version stable 2>/dev/null) || stable_pub=""
|
if ! all_arches_at_version stable "$version"; then
|
||||||
if [[ "${stable_pub%-*}" != "$version" ]]; then
|
print_error "Promotion ran but stable does not serve $version on every architecture"
|
||||||
print_error "Promotion ran but stable serves '${stable_pub:-nothing}' — investigate before continuing"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
fi
|
||||||
|
|
||||||
# 4. Final pins onto master (keeps edge overlap publishing and the repo record)
|
# 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=""
|
last=$(newest_release_branch 2>/dev/null) || last=""
|
||||||
if [[ -n "$last" && "$STABLE_VER" != "<unreachable>" ]]; then
|
if [[ -n "$last" && "$STABLE_VER" != "<unreachable>" ]]; then
|
||||||
last_ver=$(branch_to_version "$last")
|
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
|
{ 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"
|
echo "ship|$last_ver is tagged but not fully shipped — resume ship"
|
||||||
return
|
return
|
||||||
@@ -924,7 +939,7 @@ next_step() { # prints "<command>|<description>"
|
|||||||
echo "ship|Final $TRAIN_VER is pinned — finish shipping (re-runs are safe)"
|
echo "ship|Final $TRAIN_VER is pinned — finish shipping (re-runs are safe)"
|
||||||
elif [[ "$pin_commit" != "$TRAIN_HEAD" ]]; then
|
elif [[ "$pin_commit" != "$TRAIN_HEAD" ]]; then
|
||||||
echo "rc|$TRAIN has commits newer than $pin_ver — cut the next candidate"
|
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"
|
echo "rc|$pin_ver is pinned but not published — re-run rc to re-trigger/wait"
|
||||||
else
|
else
|
||||||
echo "ship|$pin_ver is published to rc — test it, then ship"
|
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 "makepkg available (checksums)" command -v makepkg
|
||||||
check "curl available" command -v curl
|
check "curl available" command -v curl
|
||||||
check "upstream reachable ($UPSTREAM_URL)" git ls-remote "$UPSTREAM_URL" HEAD
|
check "upstream reachable ($UPSTREAM_URL)" git ls-remote "$UPSTREAM_URL" HEAD
|
||||||
local ch
|
local ch arch
|
||||||
for ch in edge stable; do
|
for ch in edge stable; do
|
||||||
if published_version "$ch" >/dev/null 2>&1; then
|
for arch in $(published_arches); do
|
||||||
print_success "$ch channel db readable"
|
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
|
else
|
||||||
print_error "$ch channel db readable"
|
print_warning "rc/$arch channel db not readable — bootstrap it first: bin/repo bootstrap-rc (on the build host)"
|
||||||
failures=$((failures + 1))
|
|
||||||
fi
|
fi
|
||||||
done
|
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
|
local host
|
||||||
if host=$(repo_host); then
|
if host=$(repo_host); then
|
||||||
check "build host ssh ($host)" ssh -o ConnectTimeout=10 "$host" true
|
check "build host ssh ($host)" ssh -o ConnectTimeout=10 "$host" true
|
||||||
@@ -1016,10 +1035,10 @@ cmd_doctor() {
|
|||||||
else
|
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"
|
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
|
fi
|
||||||
if command -v docker >/dev/null && docker info >/dev/null 2>&1; then
|
if [[ -n "$CONTAINER_ENGINE" ]] && "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
|
||||||
print_success "docker available (ISO builds possible here)"
|
print_success "$CONTAINER_ENGINE available (ISO builds possible here)"
|
||||||
else
|
else
|
||||||
print_warning "docker unavailable — ISO builds will print instructions instead"
|
print_warning "container engine unavailable — ISO builds will print instructions instead"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
if ((failures == 0)); then
|
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 "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.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"
|
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 ""
|
echo ""
|
||||||
if ((failures == 0)); then
|
if ((failures == 0)); then
|
||||||
print_success "Self-test passed"
|
print_success "Self-test passed"
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ Usage: $0 <package> [OPTIONS]
|
|||||||
Create a scratch workspace for inspecting an AUR-backed package.
|
Create a scratch workspace for inspecting an AUR-backed package.
|
||||||
|
|
||||||
The workspace contains:
|
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
|
patched/ Raw AUR package with Omarchy .omarchy patches/hooks/pkgrel applied
|
||||||
current/ Current checked-in package directory
|
current/ Current checked-in package directory
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--dir <path> Workspace directory (default: mktemp under /tmp)
|
--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
|
-h, --help Show this help message
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -75,13 +75,13 @@ if [[ ! -f "$METADATA" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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"
|
print_error "package-worktree only supports AUR-backed packages"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
AUR_PACKAGE=$(jq -r --arg package "$PACKAGE" '.aur // $package' "$METADATA")
|
AUR_PACKAGE=$(jq -r --arg package "$PACKAGE" '.origin.aur // .aur // $package' "$METADATA")
|
||||||
UPSTREAM_COMMIT=${COMMIT_OVERRIDE:-$(jq -r '.upstream_commit // ""' "$METADATA")}
|
UPSTREAM_COMMIT=${COMMIT_OVERRIDE:-$(jq -r '.origin.commit // .upstream_commit // ""' "$METADATA")}
|
||||||
|
|
||||||
if [[ -z "$DEST_DIR" ]]; then
|
if [[ -z "$DEST_DIR" ]]; then
|
||||||
DEST_DIR=$(mktemp -d "${TMPDIR:-/tmp}/omarchy-${PACKAGE}.XXXXXX")
|
DEST_DIR=$(mktemp -d "${TMPDIR:-/tmp}/omarchy-${PACKAGE}.XXXXXX")
|
||||||
@@ -224,7 +224,7 @@ print_info "AUR package: $AUR_PACKAGE"
|
|||||||
if [[ -n "$UPSTREAM_COMMIT" ]]; then
|
if [[ -n "$UPSTREAM_COMMIT" ]]; then
|
||||||
print_info "Upstream commit: $UPSTREAM_COMMIT"
|
print_info "Upstream commit: $UPSTREAM_COMMIT"
|
||||||
else
|
else
|
||||||
print_warning "No upstream_commit recorded; using AUR HEAD"
|
print_warning "No origin.commit recorded; using AUR HEAD"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -rf "$UPSTREAM_DIR" "$PATCHED_DIR" "$CURRENT_DIR"
|
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
|
# leave nvidia-580xx-dkms and opencl-nvidia-580xx behind. An output's own name
|
||||||
# still matches, for pushing just one of them on purpose.
|
# still matches, for pushing just one of them on purpose.
|
||||||
#
|
#
|
||||||
# pkgbase comes from .PKGINFO rather than the PKGBUILD: it is what makepkg
|
# Package identity comes from .PKGINFO rather than the filename or PKGBUILD: it
|
||||||
# actually recorded, and it needs no guessing about which directory built what.
|
# is what makepkg actually recorded, and it handles epochs and split packages.
|
||||||
pkgbase_of() {
|
package_identity_of() {
|
||||||
bsdtar -xOf "$1" .PKGINFO 2>/dev/null |
|
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=()
|
FILES=()
|
||||||
@@ -161,14 +166,18 @@ if [[ -z "$PACKAGES" ]]; then
|
|||||||
FILES=("${ALL_FILES[@]}")
|
FILES=("${ALL_FILES[@]}")
|
||||||
else
|
else
|
||||||
declare -A MATCHED=()
|
declare -A MATCHED=()
|
||||||
|
declare -A LATEST_FILE=()
|
||||||
|
declare -A LATEST_VERSION=()
|
||||||
for file in "${ALL_FILES[@]}"; do
|
for file in "${ALL_FILES[@]}"; do
|
||||||
# name-version-release-arch.pkg.tar.zst -> name
|
IFS=$'\t' read -r pkgname pkgbase pkgver < <(package_identity_of "$BUILD_OUTPUT_DIR/$file")
|
||||||
pkgname="${file%-*-*-*.pkg.tar.*}"
|
|
||||||
pkgbase=$(pkgbase_of "$BUILD_OUTPUT_DIR/$file")
|
|
||||||
for wanted in $PACKAGES; do
|
for wanted in $PACKAGES; do
|
||||||
if [[ "$pkgname" == "$wanted" || "$pkgbase" == "$wanted" ]]; then
|
if [[ "$pkgname" == "$wanted" || "$pkgbase" == "$wanted" ]]; then
|
||||||
FILES+=("$file")
|
|
||||||
MATCHED["$wanted"]=1
|
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
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -181,6 +190,8 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
mapfile -t FILES < <(printf '%s\n' "${LATEST_FILE[@]}" | sort)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ${#FILES[@]} -eq 0 ]]; then
|
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
|
# 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.
|
# this run. Absent for a manual run, which is fine — the report just omits it.
|
||||||
QUEUED_PACKAGES=""
|
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")
|
[[ -s "$QUEUE_FILE" ]] && QUEUED_PACKAGES=$(grep -c '' "$QUEUE_FILE")
|
||||||
|
|
||||||
if [[ "$DRY_RUN" != true ]]; then
|
if [[ "$DRY_RUN" != true ]]; then
|
||||||
@@ -134,11 +138,43 @@ if [[ "$DRY_RUN" == true ]]; then
|
|||||||
else
|
else
|
||||||
print_info "Step 1/6: Building packages..."
|
print_info "Step 1/6: Building packages..."
|
||||||
fi
|
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"
|
print_error "Build failed"
|
||||||
notify_error "Release failed: Build step failed" "$RELEASE_CONTEXT"
|
notify_error "Release failed: Build step failed" "$RELEASE_CONTEXT"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
fi
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == true ]]; then
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
@@ -151,6 +187,12 @@ BUILT_COUNT=$(grep -c '' <<<"$BUILT_FILES")
|
|||||||
[[ -z "$BUILT_FILES" ]] && BUILT_COUNT=0
|
[[ -z "$BUILT_FILES" ]] && BUILT_COUNT=0
|
||||||
print_info "Built $BUILT_COUNT package(s) this run"
|
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
|
# Step 2: Sign
|
||||||
echo ""
|
echo ""
|
||||||
print_info "Step 2/6: Signing packages..."
|
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+="<br><br><strong>$BUILT_COUNT package(s) published:</strong>"
|
||||||
summary+="$(format_package_list_html "$BUILT_FILES")"
|
summary+="$(format_package_list_html "$BUILT_FILES")"
|
||||||
summary+="<br><br>Live at https://pkgs.omarchy.org/$MIRROR/$ARCH/"
|
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
|
else
|
||||||
# Rare by construction: a release only runs when the version check queued
|
# Rare by construction: a release only runs when the version check queued
|
||||||
# work, so publishing nothing means the check and the builder disagreed
|
# work, so publishing nothing means the check and the builder disagreed
|
||||||
@@ -224,4 +275,10 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
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!"
|
print_success "Release workflow completed successfully!"
|
||||||
+15
-10
@@ -62,8 +62,8 @@ if [[ ! -d "$REPO_DIR" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check Docker is available
|
# Check the selected container engine is available
|
||||||
check_docker
|
check_engine
|
||||||
|
|
||||||
# Find package files to confirm before running Docker
|
# Find package files to confirm before running Docker
|
||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
@@ -88,24 +88,29 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build/update the Docker image (always use x86_64 for removal - it's architecture independent)
|
# repo-remove is architecture-independent, so use a host-native edge image.
|
||||||
# repo-remove is mirror-independent — always use the edge x86_64 image
|
TOOL_ARCH=$(docker_native_arch) || {
|
||||||
build_docker_image "$BUILD_DIR" "x86_64" "edge"
|
print_error "Unsupported host architecture: $(uname -m)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
build_docker_image "$BUILD_DIR" "$TOOL_ARCH" "edge"
|
||||||
|
|
||||||
acquire_release_lock || exit 1
|
acquire_release_lock || exit 1
|
||||||
|
|
||||||
print_info "Removing package..."
|
print_info "Removing package..."
|
||||||
|
|
||||||
# Ensure directory is writable by container user
|
# Rootless Podman uses keep-id and leaves host ownership/modes intact.
|
||||||
make_dir_writable "$REPO_DIR"
|
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||||
|
make_dir_writable "$REPO_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
# Run the removal script in Docker (always use x86_64 image)
|
# Run the removal script in the host-native image.
|
||||||
docker run --rm --platform linux/amd64 \
|
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$TOOL_ARCH")" \
|
||||||
-e ARCH="$ARCH" \
|
-e ARCH="$ARCH" \
|
||||||
-e MIRROR="$MIRROR" \
|
-e MIRROR="$MIRROR" \
|
||||||
-v "$REPO_ROOT:/pkgs.omarchy.org" \
|
-v "$REPO_ROOT:/pkgs.omarchy.org" \
|
||||||
-v "$BUILD_DIR:/build:ro" \
|
-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=$?
|
RESULT=$?
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ set -e
|
|||||||
|
|
||||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||||
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
source "$BUILD_ROOT/helpers/message-helpers.sh"
|
||||||
|
source "$BUILD_ROOT/helpers/docker-helpers.sh"
|
||||||
|
source "$BUILD_ROOT/helpers/paths.sh"
|
||||||
|
|
||||||
CHECK_ONLY=false
|
CHECK_ONLY=false
|
||||||
SKIP_TIMERS=false
|
SKIP_TIMERS=false
|
||||||
@@ -61,10 +63,12 @@ if command -v apt-get >/dev/null 2>&1; then
|
|||||||
DISTRO="debian"
|
DISTRO="debian"
|
||||||
PKG_BSDTAR="libarchive-tools"
|
PKG_BSDTAR="libarchive-tools"
|
||||||
PKG_DOCKER="docker.io"
|
PKG_DOCKER="docker.io"
|
||||||
|
PKG_PODMAN="podman"
|
||||||
elif command -v pacman >/dev/null 2>&1; then
|
elif command -v pacman >/dev/null 2>&1; then
|
||||||
DISTRO="arch"
|
DISTRO="arch"
|
||||||
PKG_BSDTAR="libarchive"
|
PKG_BSDTAR="libarchive"
|
||||||
PKG_DOCKER="docker"
|
PKG_DOCKER="docker"
|
||||||
|
PKG_PODMAN="podman"
|
||||||
else
|
else
|
||||||
print_error "Unsupported distribution — need apt-get or pacman"
|
print_error "Unsupported distribution — need apt-get or pacman"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -72,18 +76,36 @@ fi
|
|||||||
|
|
||||||
print_info "Distribution: $DISTRO"
|
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
|
if [[ "$CHECK_ONLY" != true && $EUID -ne 0 ]]; then
|
||||||
print_error "Run as root (installing packages and systemd units)"
|
print_error "Run as root (installing packages and systemd units)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Docker and the release timers are both systemd units. Say so plainly rather
|
# The release timers are systemd units. Say so plainly rather than failing
|
||||||
# than failing later on a missing command — a container is the usual way to end
|
# later on a missing command — a container is the usual way to end up here,
|
||||||
# up here, and it cannot be a repository host.
|
# and it cannot be a repository host.
|
||||||
if [[ "$CHECK_ONLY" != true ]] && ! command -v systemctl >/dev/null 2>&1; then
|
if [[ "$CHECK_ONLY" != true ]] && ! command -v systemctl >/dev/null 2>&1; then
|
||||||
print_error "systemctl not found — the repository host must run systemd"
|
print_error "systemctl not found — the repository host must run systemd"
|
||||||
echo ""
|
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."
|
echo "container; run setup on the host itself."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -139,24 +161,27 @@ else
|
|||||||
fi
|
fi
|
||||||
echo ""
|
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
|
# version from Docker's own repository rather than the distribution's, and
|
||||||
# replacing that underneath a working builder would be a poor trade for
|
# replacing that underneath a working builder would be a poor trade for
|
||||||
# tidiness.
|
# tidiness.
|
||||||
print_info "Checking Docker..."
|
print_info "Checking $ENGINE_NAME..."
|
||||||
|
|
||||||
if command -v docker >/dev/null 2>&1; then
|
if command -v "$CONTAINER_ENGINE" >/dev/null 2>&1; then
|
||||||
print_step "docker present: $(docker --version 2>/dev/null | head -1)"
|
print_step "$CONTAINER_ENGINE present: $("$CONTAINER_ENGINE" --version 2>/dev/null | head -1)"
|
||||||
if docker info >/dev/null 2>&1; then
|
if "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
|
||||||
print_success "Docker is installed and running — leaving it alone"
|
print_success "$ENGINE_NAME is installed and available — leaving it alone"
|
||||||
elif [[ "$CHECK_ONLY" == true ]]; then
|
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
|
else
|
||||||
print_info "Docker is installed but not running — starting it"
|
print_info "Docker is installed but not running — starting it"
|
||||||
systemctl enable --now docker.service
|
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"
|
print_success "Docker started"
|
||||||
else
|
else
|
||||||
print_error "Docker is installed but still not responding"
|
print_error "Docker is installed but still not responding"
|
||||||
@@ -165,16 +190,24 @@ if command -v docker >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
elif [[ "$CHECK_ONLY" == true ]]; then
|
elif [[ "$CHECK_ONLY" == true ]]; then
|
||||||
print_warning "Would install $PKG_DOCKER and enable it"
|
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||||
else
|
print_warning "Would install $PKG_ENGINE and enable it"
|
||||||
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"
|
|
||||||
else
|
else
|
||||||
print_error "Docker installed but not responding"
|
print_warning "Would install $PKG_ENGINE"
|
||||||
echo " Check 'systemctl status docker' — builds cannot run without it."
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -237,6 +270,10 @@ echo ""
|
|||||||
|
|
||||||
# --- release timers ----------------------------------------------------------
|
# --- 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)
|
TIMERS=(omarchy-check-versions omarchy-auto-release-edge omarchy-auto-release-rc omarchy-auto-release-stable)
|
||||||
|
|
||||||
if [[ "$SKIP_TIMERS" == true ]]; then
|
if [[ "$SKIP_TIMERS" == true ]]; then
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ if [[ ! -d "$BUILD_OUTPUT_DIR" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check Docker is available
|
# Check the selected container engine is available
|
||||||
check_docker
|
check_engine
|
||||||
|
|
||||||
# Check GPG credentials are in environment
|
# Check GPG credentials are in environment
|
||||||
if [[ -z "$GPG_PRIVATE_KEY" ]]; then
|
if [[ -z "$GPG_PRIVATE_KEY" ]]; then
|
||||||
@@ -66,23 +66,30 @@ if [[ -z "$GPG_PASSPHRASE" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build/update the Docker image (always use x86_64 for signing - it's architecture independent)
|
# Signing is architecture-independent, so run its utility container natively
|
||||||
build_docker_image "$BUILD_DIR" "x86_64" "$MIRROR"
|
# 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..."
|
print_info "Running package signing..."
|
||||||
|
|
||||||
# Ensure output directory is writable by container user
|
# Rootless Podman uses keep-id and leaves host ownership/modes intact.
|
||||||
make_dir_writable "$BUILD_OUTPUT_DIR"
|
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||||
|
make_dir_writable "$BUILD_OUTPUT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
# Run the signing script in Docker (always use x86_64 image)
|
# Run the signing script in the host-native image.
|
||||||
docker run --rm --platform linux/amd64 \
|
"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$TOOL_ARCH")" \
|
||||||
-e ARCH="$ARCH" \
|
-e ARCH="$ARCH" \
|
||||||
-e MIRROR="$MIRROR" \
|
-e MIRROR="$MIRROR" \
|
||||||
-e GPG_PRIVATE_KEY="$GPG_PRIVATE_KEY" \
|
-e GPG_PRIVATE_KEY="$GPG_PRIVATE_KEY" \
|
||||||
-e GPG_PASSPHRASE="$GPG_PASSPHRASE" \
|
-e GPG_PASSPHRASE="$GPG_PASSPHRASE" \
|
||||||
-v "$BUILD_ROOT/build-output:/build-output" \
|
-v "$BUILD_ROOT/build-output:/build-output" \
|
||||||
-v "$BUILD_DIR:/build:ro" \
|
-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=$?
|
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
|
|
||||||
+470
-75
@@ -17,11 +17,14 @@ SELF_TEST=false
|
|||||||
# rebuilding for it would ship a package built against the wrong ABI.
|
# rebuilding for it would ship a package built against the wrong ABI.
|
||||||
OFFICIAL_REPOS=" core extra multilib core-debug extra-debug "
|
OFFICIAL_REPOS=" core extra multilib core-debug extra-debug "
|
||||||
|
|
||||||
# The published repository, used as the floor a bumped pkgrel has to clear.
|
# The published repositories are the floor a bumped pkgrel has to clear.
|
||||||
# Only x86_64 is published today; aarch64 has no repository to compare against.
|
|
||||||
PUBLISHED_BASE_URL="${OMARCHY_PUBLISHED_BASE_URL:-https://pkgs.omarchy.org}"
|
PUBLISHED_BASE_URL="${OMARCHY_PUBLISHED_BASE_URL:-https://pkgs.omarchy.org}"
|
||||||
PUBLISHED_MIRRORS=(edge stable)
|
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() {
|
usage() {
|
||||||
cat <<EOF
|
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"] }
|
{ "source": "aur", "sync": false, "rebuild_on": ["qt6-base"] }
|
||||||
|
|
||||||
The versions the current pkgrel was bumped for are recorded alongside, in
|
The versions the current pkgrel was bumped for are recorded per published
|
||||||
rebuilt_against, and written by this command:
|
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
|
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
|
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 # Update every package that declares rebuild_on
|
||||||
$0 quickshell-git # Update specific packages
|
$0 quickshell-git # Update specific packages
|
||||||
|
|
||||||
Trigger versions are read from the local pacman database, so sync it first
|
x86_64 trigger versions come from the local pacman database; aarch64 versions
|
||||||
(pacman -Sy) or this reports whatever that database last saw.
|
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
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +103,7 @@ SPECIFIC_MODE=false
|
|||||||
# The version of a trigger package as the build container would resolve it.
|
# 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
|
# 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.
|
# gets to say which package was left alone instead of the run dying here.
|
||||||
repo_version() {
|
native_repo_version() {
|
||||||
local package="$1"
|
local package="$1"
|
||||||
local info
|
local info
|
||||||
|
|
||||||
@@ -108,9 +117,61 @@ repo_version() {
|
|||||||
' <<<"$info"
|
' <<<"$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 PUBLISHED_VERSION=()
|
||||||
|
declare -A EDGE_VERSION=()
|
||||||
|
declare -A EDGE_READ=()
|
||||||
PUBLISHED_LOADED=false
|
PUBLISHED_LOADED=false
|
||||||
PUBLISHED_AVAILABLE=true
|
|
||||||
|
|
||||||
remember_published() {
|
remember_published() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
@@ -131,34 +192,44 @@ load_published_versions() {
|
|||||||
[[ "$PUBLISHED_LOADED" == true ]] && return 0
|
[[ "$PUBLISHED_LOADED" == true ]] && return 0
|
||||||
PUBLISHED_LOADED=true
|
PUBLISHED_LOADED=true
|
||||||
|
|
||||||
local mirror db name base version
|
local arch mirror db name base version
|
||||||
|
|
||||||
for mirror in "${PUBLISHED_MIRRORS[@]}"; do
|
for arch in $(ci_arches); do
|
||||||
db="$TEMP_DIR/published-$mirror.db.tar.zst"
|
for mirror in "${PUBLISHED_MIRRORS[@]}"; do
|
||||||
|
db="$TEMP_DIR/published-$mirror-$arch.db.tar.zst"
|
||||||
|
|
||||||
if ! curl -fsSL --max-time 120 -o "$db" \
|
if ! curl -fsSL --max-time 120 -o "$db" \
|
||||||
"$PUBLISHED_BASE_URL/$mirror/$PUBLISHED_ARCH/omarchy.db.tar.zst" 2>/dev/null; then
|
"$PUBLISHED_BASE_URL/$mirror/$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"
|
print_warning "Could not read the published $mirror/$arch database; bumps are not checked against it this run"
|
||||||
PUBLISHED_AVAILABLE=false
|
continue
|
||||||
continue
|
fi
|
||||||
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
|
while IFS=$'\t' read -r name base version; do
|
||||||
[[ -n "$name" && -n "$version" ]] && remember_published "$name" "$version"
|
[[ -n "$name" && -n "$version" ]] && remember_published "$name" "$version"
|
||||||
[[ -n "$base" && -n "$version" ]] && remember_published "$base" "$version"
|
[[ -n "$base" && -n "$version" ]] && remember_published "$base" "$version"
|
||||||
done < <(
|
if [[ "$mirror" == edge && -n "$version" ]]; then
|
||||||
tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk '
|
[[ -z "$name" ]] || EDGE_VERSION["$arch/$name"]="$version"
|
||||||
function emit() {
|
[[ -z "$base" ]] || EDGE_VERSION["$arch/$base"]="$version"
|
||||||
if (name != "" && version != "") print name "\t" base "\t" version
|
fi
|
||||||
name=""; base=""; version=""
|
done < <(
|
||||||
}
|
tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk '
|
||||||
$0 == "%FILENAME%" { emit(); next }
|
function emit() {
|
||||||
$0 == "%NAME%" { if (name != "" && version != "") emit(); getline; name=$0; next }
|
if (name != "" && version != "") print name "\t" base "\t" version
|
||||||
$0 == "%BASE%" { getline; base=$0; next }
|
name=""; base=""; version=""
|
||||||
$0 == "%VERSION%" { getline; version=$0; next }
|
}
|
||||||
END { emit() }
|
$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
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +240,29 @@ published_version() {
|
|||||||
echo "${PUBLISHED_VERSION[$package]:-}"
|
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.
|
# The pkgver of a full version string, with any epoch and pkgrel removed.
|
||||||
version_pkgver() {
|
version_pkgver() {
|
||||||
local version="${1#*:}"
|
local version="${1#*:}"
|
||||||
@@ -179,7 +273,7 @@ pkgbuild_field() {
|
|||||||
local package_dir="$1"
|
local package_dir="$1"
|
||||||
local field="$2"
|
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
|
# 2 -> 3, and 1.1 -> 1.2. Anything else is a pkgrel this command has no business
|
||||||
@@ -235,6 +329,43 @@ write_pkgrel() {
|
|||||||
mv "$scratch" "$pkgbuild"
|
mv "$scratch" "$pkgbuild"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# A package built against a trigger's ABI pins it exactly, once per architecture
|
||||||
|
# because Arch Linux ARM can trail x86_64: depends_x86_64=('hyprland=0.56.2').
|
||||||
|
# Rebuilding against a newer trigger has to move that pin with it, or the
|
||||||
|
# rebuilt package demands the version it no longer loads into. Unpinned
|
||||||
|
# dependencies, and pins outside the per-architecture arrays, are left alone.
|
||||||
|
write_pins() {
|
||||||
|
local package_dir="$1"
|
||||||
|
local current="$2"
|
||||||
|
local pkgbuild="$package_dir/PKGBUILD"
|
||||||
|
local scratch="$pkgbuild.sync-rebuilds"
|
||||||
|
local arch trigger version pattern
|
||||||
|
|
||||||
|
cp "$pkgbuild" "$scratch" || return 1
|
||||||
|
while IFS=$'\t' read -r arch trigger version; do
|
||||||
|
pattern=$(printf '%s' "$trigger" | sed 's/[][\.*^$+?(){}|/]/\\&/g')
|
||||||
|
if ! awk -v array="depends_$arch" -v pattern="$pattern" -v pin="$trigger=$(version_pkgver "$version")" '
|
||||||
|
$0 ~ "^" array "=\\(" { inside = 1 }
|
||||||
|
inside { gsub("[\"\047]" pattern "=[^\"\047]*[\"\047]", "\047" pin "\047") }
|
||||||
|
inside && /\)/ { inside = 0 }
|
||||||
|
{ print }
|
||||||
|
' "$scratch" > "$scratch.new"; then
|
||||||
|
rm -f "$scratch" "$scratch.new"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
mv "$scratch.new" "$scratch"
|
||||||
|
done < <(jq -r 'to_entries[] | .key as $arch | .value | to_entries[] | [$arch, .key, .value] | @tsv' <<<"$current")
|
||||||
|
|
||||||
|
if ! bash -n "$scratch" 2>/dev/null; then
|
||||||
|
print_error "PKGBUILD with moved pins is not valid shell"
|
||||||
|
rm -f "$scratch"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod --reference="$pkgbuild" "$scratch"
|
||||||
|
mv "$scratch" "$pkgbuild"
|
||||||
|
}
|
||||||
|
|
||||||
write_metadata() {
|
write_metadata() {
|
||||||
local package_dir="$1"
|
local package_dir="$1"
|
||||||
local filter="$2"
|
local filter="$2"
|
||||||
@@ -252,7 +383,17 @@ record_triggers() {
|
|||||||
local package_dir="$1"
|
local package_dir="$1"
|
||||||
local current="$2"
|
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
|
# Metadata that does not parse would otherwise drop its package out of the run
|
||||||
@@ -300,21 +441,61 @@ sync_package() {
|
|||||||
|
|
||||||
print_info "Checking $package against ${triggers[*]}..."
|
print_info "Checking $package against ${triggers[*]}..."
|
||||||
|
|
||||||
local current="{}" trigger version
|
local current="{}" arch arch_current trigger version carried considered=0
|
||||||
for trigger in "${triggers[@]}"; do
|
for arch in $(ci_arches); do
|
||||||
version=$(repo_version "$trigger")
|
package_supports_arch "$package_dir" "$arch" || continue
|
||||||
if [[ -z "$version" ]]; then
|
considered=$((considered + 1))
|
||||||
print_error " $trigger is in no official repository; leaving $package alone"
|
arch_current="{}"
|
||||||
((++FAILED))
|
for trigger in "${triggers[@]}"; do
|
||||||
return 0
|
if carried=$(carried_trigger_dir "$arch" "$trigger"); then
|
||||||
fi
|
if ! version=$(carried_version "$arch" "$carried"); then
|
||||||
if ! current=$(jq -c --arg name "$trigger" --arg version "$version" '.[$name] = $version' <<<"$current"); then
|
print_error " Could not read the $arch version of $trigger from its recipe; leaving $package alone"
|
||||||
print_error " Could not record $trigger $version for $package"
|
((++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))
|
((++FAILED))
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if ((considered == 0)); then
|
||||||
|
print_info " Skipping: not built for any published architecture"
|
||||||
|
((++SKIPPED))
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
local recorded
|
local recorded
|
||||||
if ! recorded=$(package_metadata_value "$package_dir" '.rebuilt_against' ""); then
|
if ! recorded=$(package_metadata_value "$package_dir" '.rebuilt_against' ""); then
|
||||||
print_error " Could not read .omarchy/package.json for $package"
|
print_error " Could not read .omarchy/package.json for $package"
|
||||||
@@ -323,13 +504,20 @@ sync_package() {
|
|||||||
fi
|
fi
|
||||||
[[ -n "$recorded" && "$recorded" != "null" ]] || recorded="{}"
|
[[ -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
|
# Walk the declared triggers rather than the record, so a name the record does
|
||||||
# not carry reads as changed instead of going unexamined forever.
|
# not carry reads as changed instead of going unexamined forever.
|
||||||
local moved
|
local moved
|
||||||
if ! moved=$(jq -r --argjson recorded "$recorded" '
|
if ! moved=$(jq -r --argjson recorded "$recorded" '
|
||||||
to_entries
|
[to_entries[] as $arch
|
||||||
| map(select($recorded[.key] != .value)
|
| $arch.value | to_entries[] as $trigger
|
||||||
| "\(.key) \($recorded[.key] // "unrecorded") -> \(.value)")
|
| select($recorded[$arch.key][$trigger.key] != $trigger.value)
|
||||||
|
| "\($arch.key)/\($trigger.key) \($recorded[$arch.key][$trigger.key] // "unrecorded") -> \($trigger.value)"]
|
||||||
| join(", ")
|
| join(", ")
|
||||||
' <<<"$current"); then
|
' <<<"$current"); then
|
||||||
print_error " Could not compare recorded trigger versions for $package"
|
print_error " Could not compare recorded trigger versions for $package"
|
||||||
@@ -338,7 +526,7 @@ sync_package() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$moved" ]]; then
|
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))
|
((++SKIPPED))
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -384,6 +572,7 @@ sync_package() {
|
|||||||
# checkout that has fallen behind the repository can otherwise be bumped to
|
# checkout that has fallen behind the repository can otherwise be bumped to
|
||||||
# something pacman orders below what it would replace.
|
# something pacman orders below what it would replace.
|
||||||
local floor
|
local floor
|
||||||
|
load_published_versions
|
||||||
floor=$(published_version "$package")
|
floor=$(published_version "$package")
|
||||||
if [[ -n "$floor" && "$(version_pkgver "$floor")" == "$pkgver" ]]; then
|
if [[ -n "$floor" && "$(version_pkgver "$floor")" == "$pkgver" ]]; then
|
||||||
if [[ "$(vercmp "$new_version" "$floor")" -le 0 ]]; then
|
if [[ "$(vercmp "$new_version" "$floor")" -le 0 ]]; then
|
||||||
@@ -399,6 +588,12 @@ sync_package() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if ! write_pins "$package_dir" "$current"; then
|
||||||
|
print_error " Failed to move pinned trigger versions for $package"
|
||||||
|
((++FAILED))
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -n "$suffix" ]] && ! write_metadata "$package_dir" '.pkgrel.suffix = ($suffix | tonumber)' --arg suffix "$suffix"; then
|
if [[ -n "$suffix" ]] && ! write_metadata "$package_dir" '.pkgrel.suffix = ($suffix | tonumber)' --arg suffix "$suffix"; then
|
||||||
print_error " Failed to record pkgrel suffix for $package"
|
print_error " Failed to record pkgrel suffix for $package"
|
||||||
((++FAILED))
|
((++FAILED))
|
||||||
@@ -466,11 +661,11 @@ selftest_root() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selftest_package() {
|
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"
|
local dir="$root/pkgbuilds/$name"
|
||||||
|
|
||||||
mkdir -p "$dir/.omarchy"
|
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"
|
printf '%s\n' "$metadata" > "$dir/.omarchy/package.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,44 +684,97 @@ STUB
|
|||||||
chmod +x "$root/stub/pacman"
|
chmod +x "$root/stub/pacman"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Serves a repository database assembled by hand from name=version pairs. With
|
# A repository database assembled by hand from name=version pairs.
|
||||||
# none given the stub fails, which is how the unreachable-repository path is
|
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.
|
# exercised.
|
||||||
selftest_published() {
|
selftest_published() {
|
||||||
local root="$1"
|
local root="$1"
|
||||||
shift
|
shift
|
||||||
local staging="$root/stub/db"
|
|
||||||
local entry name version
|
|
||||||
|
|
||||||
if [[ $# -gt 0 ]]; then
|
if [[ $# -gt 0 ]]; then
|
||||||
rm -rf "$staging"
|
selftest_db "$root/stub/omarchy.db.tar.zst" "$@"
|
||||||
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" .
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat > "$root/stub/curl" <<'STUB'
|
cat > "$root/stub/curl" <<'STUB'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
out=""
|
out=""
|
||||||
|
url=""
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-o) out="$2"; shift 2 ;;
|
-o) out="$2"; shift 2 ;;
|
||||||
*) shift ;;
|
*) url="$1"; shift ;;
|
||||||
esac
|
esac
|
||||||
done
|
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
|
[[ -f "$db" && -n "$out" ]] || exit 22
|
||||||
cp "$db" "$out"
|
cp "$db" "$out"
|
||||||
STUB
|
STUB
|
||||||
chmod +x "$root/stub/curl"
|
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() {
|
cmd_self_test() {
|
||||||
local failures=0
|
local failures=0
|
||||||
local root
|
local root
|
||||||
@@ -541,11 +789,15 @@ cmd_self_test() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# SELFTEST_ARCHES stands in for CI_ARCHES; "default" leaves it unset, as the
|
||||||
|
# scheduled workflow does.
|
||||||
run_case() {
|
run_case() {
|
||||||
local root="$1"
|
local root="$1"
|
||||||
shift
|
shift
|
||||||
local status=0
|
local status=0 arches=(CI_ARCHES="${SELFTEST_ARCHES:-x86_64}")
|
||||||
PATH="$root/stub:$PATH" "$root/bin/sync-rebuilds" "$@" > "$root/output" 2>&1 || status=$?
|
[[ "${SELFTEST_ARCHES:-}" != default ]] || arches=(-u CI_ARCHES)
|
||||||
|
env "${arches[@]}" PATH="$root/stub:$PATH" \
|
||||||
|
"$root/bin/sync-rebuilds" "$@" > "$root/output" 2>&1 || status=$?
|
||||||
echo "$status"
|
echo "$status"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,7 +815,7 @@ cmd_self_test() {
|
|||||||
check "run succeeds" 0 "$(run_case "$root")"
|
check "run succeeds" 0 "$(run_case "$root")"
|
||||||
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-partial")"
|
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-partial")"
|
||||||
check "unrecorded trigger now recorded" "2-2" \
|
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:"
|
echo "Opting a package in buys a rebuild rather than a bare record:"
|
||||||
root=$(selftest_root fresh)
|
root=$(selftest_root fresh)
|
||||||
@@ -573,7 +825,7 @@ cmd_self_test() {
|
|||||||
check "run succeeds" 0 "$(run_case "$root")"
|
check "run succeeds" 0 "$(run_case "$root")"
|
||||||
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-fresh")"
|
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-fresh")"
|
||||||
check "trigger recorded" "1-1" \
|
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:"
|
echo "An unchanged package is left alone:"
|
||||||
root=$(selftest_root current)
|
root=$(selftest_root current)
|
||||||
@@ -583,6 +835,17 @@ cmd_self_test() {
|
|||||||
check "run succeeds" 0 "$(run_case "$root")"
|
check "run succeeds" 0 "$(run_case "$root")"
|
||||||
check "pkgrel untouched" 1 "$(pkgrel_of "$root/pkgbuilds/t-current")"
|
check "pkgrel untouched" 1 "$(pkgrel_of "$root/pkgbuilds/t-current")"
|
||||||
|
|
||||||
|
echo "A rebuild moves the trigger's exact pin for the architecture it ran for:"
|
||||||
|
root=$(selftest_root pins)
|
||||||
|
selftest_package "$root" t-pins 1 '{"source":"local","rebuild_on":["dep-a"],"rebuilt_against":{"dep-a":"1.0-1"}}'
|
||||||
|
printf "depends_x86_64=('dep-a=1.0' 'dep-b')\ndepends_aarch64=('dep-a=1.0')\n" >> "$root/pkgbuilds/t-pins/PKGBUILD"
|
||||||
|
selftest_pacman "$root" dep-a=2.0-1
|
||||||
|
selftest_published "$root"
|
||||||
|
check "run succeeds" 0 "$(run_case "$root")"
|
||||||
|
check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-pins")"
|
||||||
|
check "x86_64 pin moved" "depends_x86_64=('dep-a=2.0' 'dep-b')" "$(grep '^depends_x86_64=' "$root/pkgbuilds/t-pins/PKGBUILD")"
|
||||||
|
check "aarch64 pin left for its own run" "depends_aarch64=('dep-a=1.0')" "$(grep '^depends_aarch64=' "$root/pkgbuilds/t-pins/PKGBUILD")"
|
||||||
|
|
||||||
echo "Metadata that does not parse fails the run instead of vanishing from it:"
|
echo "Metadata that does not parse fails the run instead of vanishing from it:"
|
||||||
root=$(selftest_root unreadable)
|
root=$(selftest_root unreadable)
|
||||||
selftest_package "$root" t-broken 1 '{"source":"local","rebuild_on":["dep-a"'
|
selftest_package "$root" t-broken 1 '{"source":"local","rebuild_on":["dep-a"'
|
||||||
@@ -612,6 +875,138 @@ cmd_self_test() {
|
|||||||
check "suffix recorded for the next AUR sync" 1 \
|
check "suffix recorded for the next AUR sync" 1 \
|
||||||
"$(jq -r '.pkgrel.suffix' "$root/pkgbuilds/t-aur/.omarchy/package.json")"
|
"$(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 ""
|
echo ""
|
||||||
if [[ "$failures" -eq 0 ]]; then
|
if [[ "$failures" -eq 0 ]]; then
|
||||||
print_success "Self-test passed"
|
print_success "Self-test passed"
|
||||||
@@ -626,9 +1021,9 @@ if [[ "$SELF_TEST" == true ]]; then
|
|||||||
exit $?
|
exit $?
|
||||||
fi
|
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
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
+486
-23
@@ -9,8 +9,10 @@ source "$BUILD_ROOT/helpers/upstream-github.sh"
|
|||||||
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
TEMP_DIR=$(mktemp -d)
|
||||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||||
|
export UPSTREAM_CACHE_DIR="$TEMP_DIR/watch-cache"
|
||||||
|
|
||||||
SPECIFIC_PACKAGES=()
|
SPECIFIC_PACKAGES=()
|
||||||
|
LANE=all
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
@@ -18,11 +20,11 @@ Usage: $0 [PACKAGE...]
|
|||||||
|
|
||||||
Update packages that track an upstream vendor release feed instead of the AUR.
|
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
|
Packages opt in declaratively through "upstream" in .omarchy/package.json.
|
||||||
opts in declaratively, via "upstream" in .omarchy/package.json (see
|
Providers cover GitHub Releases with checksum manifests or API asset digests,
|
||||||
helpers/upstream-github.sh for the schema); no code needed. Anything with a
|
semver-shaped git tags, npm dist-tags, and plain Debian Packages indexes. See
|
||||||
bespoke feed provides pkgbuilds/<package>/.omarchy/upstream.sh instead, a hook
|
docs/upstream-sources.md for the schemas. Anything outside those conventions may provide
|
||||||
that reports the newest upstream release as JSON on stdout:
|
pkgbuilds/<package>/.omarchy/upstream.sh, a hook that reports JSON on stdout:
|
||||||
|
|
||||||
{
|
{
|
||||||
"pkgver": "1.2.3",
|
"pkgver": "1.2.3",
|
||||||
@@ -48,6 +50,14 @@ the bypass, so the resulting change still goes through a reviewed PR.
|
|||||||
Arguments:
|
Arguments:
|
||||||
PACKAGE One or more package names to update (optional)
|
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:
|
Commands:
|
||||||
self-test Run the offline fixture tests for release selection, the
|
self-test Run the offline fixture tests for release selection, the
|
||||||
quarantine backstop, and metadata parsing
|
quarantine backstop, and metadata parsing
|
||||||
@@ -64,6 +74,14 @@ while [[ $# -gt 0 ]]; do
|
|||||||
usage
|
usage
|
||||||
exit 0
|
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"
|
print_error "Unknown option: $1"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -140,17 +158,22 @@ set_pkgbuild_array() {
|
|||||||
|
|
||||||
local rewritten="$TEMP_DIR/pkgbuild-rewritten"
|
local rewritten="$TEMP_DIR/pkgbuild-rewritten"
|
||||||
if ! awk -v prefix="${name}=(" -v block="$block" '
|
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 {
|
!replaced && index($0, prefix) == 1 {
|
||||||
while ((getline line < block) > 0) print line
|
while ((getline line < block) > 0) print line
|
||||||
close(block)
|
close(block)
|
||||||
replaced = 1
|
replaced = 1
|
||||||
# A ")" anywhere past the opening closes the array; testing for one at end
|
# 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
|
# of line instead would treat a trailing comment as a continuation and eat
|
||||||
# every line up to the next ")".
|
# every line up to the next ")". A ")" inside a comment -- "# sidecar
|
||||||
if (index(substr($0, length(prefix) + 1), ")") == 0) skipping = 1
|
# (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
|
next
|
||||||
}
|
}
|
||||||
skipping { if ($0 ~ /\)/) skipping = 0; next }
|
skipping { if (code($0) ~ /\)/) skipping = 0; next }
|
||||||
{ print }
|
{ print }
|
||||||
' "$pkgbuild" > "$rewritten"; then
|
' "$pkgbuild" > "$rewritten"; then
|
||||||
print_error "Failed to rewrite ${name} in $pkgbuild"
|
print_error "Failed to rewrite ${name} in $pkgbuild"
|
||||||
@@ -332,27 +355,33 @@ sync_package() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local github_repo has_upstream=false
|
if jq -e '.sync == false' "$package_dir/.omarchy/package.json" >/dev/null 2>&1; then
|
||||||
github_repo=$(package_upstream_github_repo "$package_dir")
|
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
|
if package_has_upstream_provider "$package_dir"; then
|
||||||
has_upstream=true
|
has_upstream=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# A present-but-unusable declaration fails loudly; treating it like "no
|
# A present-but-unusable declaration fails loudly; treating it like "no
|
||||||
# upstream source" would silently drop the package from scheduled runs.
|
# upstream source" would silently drop the package from scheduled runs.
|
||||||
if [[ "$has_upstream" == true && -z "$github_repo" ]]; then
|
if [[ "$has_upstream" == true && -z "$provider" ]]; then
|
||||||
print_error "Package $package has an unusable upstream declaration (needs a github owner/repo)"
|
print_error "Package $package has an unusable or ambiguous upstream declaration"
|
||||||
((++FAILED))
|
((++FAILED))
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$github_repo" && -f "$hook" ]]; then
|
if [[ -n "$provider" && -f "$hook" ]]; then
|
||||||
print_error "Package $package declares both upstream.github and an upstream.sh hook; keep exactly one"
|
print_error "Package $package declares both an upstream provider and an upstream.sh hook; keep exactly one"
|
||||||
((++FAILED))
|
((++FAILED))
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$github_repo" && ! -f "$hook" ]]; then
|
if [[ -z "$provider" && ! -f "$hook" ]]; then
|
||||||
if [[ "$SPECIFIC_MODE" == true ]]; then
|
if [[ "$SPECIFIC_MODE" == true ]]; then
|
||||||
print_error "Package $package has neither an upstream declaration nor .omarchy/upstream.sh"
|
print_error "Package $package has neither an upstream declaration nor .omarchy/upstream.sh"
|
||||||
((++FAILED))
|
((++FAILED))
|
||||||
@@ -372,10 +401,31 @@ sync_package() {
|
|||||||
|
|
||||||
print_info "Checking $package for upstream releases..."
|
print_info "Checking $package for upstream releases..."
|
||||||
|
|
||||||
local release
|
if [[ "$provider" == watch ]]; then
|
||||||
if [[ -n "$github_repo" ]]; then
|
local result
|
||||||
if ! release=$(github_upstream_release "$package_dir" "$min_age"); then
|
if ! result=$(python3 "$BUILD_ROOT/helpers/upstream-watch.py" sync "$package_dir" --min-age "$min_age"); then
|
||||||
print_error "GitHub release provider failed for $package"
|
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))
|
((++FAILED))
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -461,8 +511,8 @@ sync_package() {
|
|||||||
# paths. Covers release selection (fallback past quarantined releases,
|
# paths. Covers release selection (fallback past quarantined releases,
|
||||||
# draft/prerelease filtering, bypass, unchanged version), failure paths
|
# draft/prerelease filtering, bypass, unchanged version), failure paths
|
||||||
# (unusable tags/timestamps, missing checksums), checksum template mapping
|
# (unusable tags/timestamps, missing checksums), checksum template mapping
|
||||||
# for both architectures, the min_release_age backstop, the duration parser,
|
# for both architectures, release API digests, the min_release_age backstop,
|
||||||
# and manifest validation.
|
# the duration parser, and manifest validation.
|
||||||
cmd_self_test() {
|
cmd_self_test() {
|
||||||
local failures=0
|
local failures=0
|
||||||
|
|
||||||
@@ -539,6 +589,31 @@ EOF
|
|||||||
check "bypass lifts the quarantine" "2.0.0" "$(jq -r '.pkgver // "<none>"' <<<"$out")"
|
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")"
|
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>"
|
out=$(github_upstream_release "$pkg" 8640000 2>/dev/null) || out="<error>"
|
||||||
check "everything quarantined reports no update" "{}" "$(jq -c . <<<"$out")"
|
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=$?
|
rc=0; github_upstream_release "$pkg" 86400 >/dev/null 2>&1 || rc=$?
|
||||||
check "missing aarch64 checksum fails the sync" "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:"
|
echo "Quarantine backstop:"
|
||||||
local rel st
|
local rel st
|
||||||
rel=$(jq -n --arg p "$old2d" '{pkgver: "1.9.0", published_at: $p, sha256sums: {}}')
|
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"
|
echo '{"source": "local", "upstream": {"github": "example/tool"}}' > "$agepkg/.omarchy/package.json"
|
||||||
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
||||||
check "upstream without checksums/assets is rejected" "1" "$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"
|
cp "$pkg/.omarchy/package.json" "$agepkg/.omarchy/package.json"
|
||||||
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
vst=0; validate_package_metadata "$agepkg" >/dev/null || vst=$?
|
||||||
check "the real declaration shape is accepted" "0" "$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
|
# End to end over the real mise-bin package: its checked-in metadata and
|
||||||
# PKGBUILD, the full sync_package path (selection, validation, backstop,
|
# PKGBUILD, the full sync_package path (selection, validation, backstop,
|
||||||
# rewrite, read-back verification), with only the two network fetches
|
# 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))"
|
check "malformed upstream fails the sync instead of skipping" "1" "$((FAILED - prev_failed))"
|
||||||
FAILED=0
|
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 ""
|
echo ""
|
||||||
if [[ "$failures" -eq 0 ]]; then
|
if [[ "$failures" -eq 0 ]]; then
|
||||||
print_success "Self-test passed"
|
print_success "Self-test passed"
|
||||||
@@ -683,16 +1062,100 @@ if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 && "${SPECIFIC_PACKAGES[0]}" == "self-test"
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
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
|
if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 ]]; then
|
||||||
SPECIFIC_MODE=true
|
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
|
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
|
done
|
||||||
else
|
else
|
||||||
while IFS= read -r package; do
|
while IFS= read -r package; do
|
||||||
sync_package "$package"
|
sync_in_lane "$package"
|
||||||
done < <(packages_for_upstream_sync)
|
done < <(packages_for_upstream_sync)
|
||||||
fi
|
fi
|
||||||
|
enforce_branch_lockstep
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
if [[ $FAILED -gt 0 ]]; then
|
if [[ $FAILED -gt 0 ]]; then
|
||||||
|
|||||||
+38
-32
@@ -84,15 +84,18 @@ echo ""
|
|||||||
|
|
||||||
# --- queued work -------------------------------------------------------------
|
# --- 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
|
queued=false
|
||||||
for channel in edge rc stable; do
|
for channel in edge rc stable; do
|
||||||
state_file="$STATE_DIR/.sync-needed-$channel"
|
for arch in $(published_arches); do
|
||||||
if [[ -f "$state_file" ]]; then
|
state_file=$(sync_queue_file "$channel" "$arch")
|
||||||
queued=true
|
[[ "$arch" == "x86_64" && ! -f "$state_file" ]] && state_file=$(legacy_sync_queue_file "$channel")
|
||||||
since=$(date -r "$state_file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")
|
if [[ -f "$state_file" ]]; then
|
||||||
printf ' • %-7s queued since %s\n' "$channel" "$since"
|
queued=true
|
||||||
fi
|
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
|
done
|
||||||
[[ "$queued" == false ]] && echo " (nothing queued — all channels up to date)"
|
[[ "$queued" == false ]] && echo " (nothing queued — all channels up to date)"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -101,34 +104,37 @@ echo ""
|
|||||||
# say so plainly: it is queued but deliberately not being retried yet.
|
# say so plainly: it is queued but deliberately not being retried yet.
|
||||||
paused=false
|
paused=false
|
||||||
for channel in edge rc stable; do
|
for channel in edge rc stable; do
|
||||||
fail_file="$STATE_DIR/.build-failed-$channel"
|
for arch in $(published_arches); do
|
||||||
[[ -f "$fail_file" ]] || continue
|
fail_file=$(sync_fail_file "$channel" "$arch")
|
||||||
if [[ "$paused" == false ]]; then
|
[[ "$arch" == "x86_64" && ! -f "$fail_file" ]] && fail_file=$(legacy_sync_fail_file "$channel")
|
||||||
print_error "Failing builds (backoff active)"
|
[[ -f "$fail_file" ]] || continue
|
||||||
paused=true
|
if [[ "$paused" == false ]]; then
|
||||||
fi
|
print_error "Failing builds (backoff active)"
|
||||||
FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT=""
|
paused=true
|
||||||
# shellcheck disable=SC1090
|
fi
|
||||||
source "$fail_file" 2>/dev/null || true
|
FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT=""
|
||||||
delay=600
|
# shellcheck disable=SC1090
|
||||||
for ((i = 1; i < FAILURE_COUNT; i++)); do
|
source "$fail_file" 2>/dev/null || true
|
||||||
delay=$((delay * 2))
|
delay=600
|
||||||
((delay >= 21600)) && { delay=21600; break; }
|
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
|
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
|
done
|
||||||
if [[ "$paused" == true ]]; then
|
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 ""
|
echo ""
|
||||||
fi
|
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/paths.sh"
|
||||||
source "$BUILD_ROOT/helpers/lock-helpers.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() {
|
update_database() {
|
||||||
print_info "Updating repository database in Docker container..."
|
print_info "Updating repository database in a container..."
|
||||||
|
|
||||||
# Check Docker is available
|
check_engine
|
||||||
check_docker
|
|
||||||
|
|
||||||
# Ensure output directory exists
|
# Ensure output directory exists
|
||||||
if [[ ! -d "$REPO_DIR" ]]; then
|
if [[ ! -d "$REPO_DIR" ]]; then
|
||||||
@@ -25,20 +24,25 @@ update_database() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Make output directory writable for container
|
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||||
make_dir_writable "$REPO_DIR"
|
make_dir_writable "$REPO_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
# repo-add is architecture- and mirror-independent, so always use the edge
|
# repo-add is architecture- and mirror-independent. Use the host-native edge
|
||||||
# x86_64 image. This also lets bootstrap-rc build the rc database before the
|
# image, which also lets bootstrap-rc run before that channel exists remotely.
|
||||||
# rc channel exists remotely (an rc image can only build after it does).
|
local tool_arch
|
||||||
build_docker_image "$BUILD_DIR" "x86_64" "edge"
|
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 ARCH="$ARCH" \
|
||||||
-e MIRROR="$MIRROR" \
|
-e MIRROR="$MIRROR" \
|
||||||
-v "$REPO_ROOT:/output" \
|
-v "$REPO_ROOT:/output" \
|
||||||
-v "$BUILD_DIR:/build:ro" \
|
-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
|
# 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; \
|
printf 'Server = https://mirror.omarchy.org/$repo/os/$arch\n' > /etc/pacman.d/mirrorlist; \
|
||||||
fi; \
|
fi; \
|
||||||
else \
|
else \
|
||||||
curl -L "https://raw.githubusercontent.com/archlinuxarm/PKGBUILDs/master/core/pacman-mirrorlist/mirrorlist" 2>/dev/null | \
|
printf 'Server = https://fl.us.mirror.archlinuxarm.org/aarch64/$repo\n' > /etc/pacman.d/mirrorlist; \
|
||||||
sed -E 's/^\s*#\s*Server\s*=/Server =/g' > /etc/pacman.d/mirrorlist && \
|
|
||||||
sed -i 's/\$arch/aarch64/g' /etc/pacman.d/mirrorlist; \
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Bootstrap keyrings (required before pacstrap can verify packages)
|
# 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)
|
# Setup Omarchy keyring manually before adding repo (avoids keyserver trust issues)
|
||||||
# Note: Repository is removed at the end since build scripts add it dynamically.
|
# 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
|
# 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
|
ARG MIRROR=edge
|
||||||
RUN pacman-key --recv-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 --keyserver keys.openpgp.org && \
|
RUN pacman-key --recv-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 --keyserver keys.openpgp.org && \
|
||||||
pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571 && \
|
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 -Sy --noconfirm && \
|
||||||
pacman -S --noconfirm omarchy-keyring && \
|
pacman -S --noconfirm omarchy-keyring && \
|
||||||
pacman-key --populate omarchy && \
|
pacman-key --populate omarchy && \
|
||||||
@@ -126,6 +129,7 @@ RUN pacman -Syu --noconfirm && \
|
|||||||
wget \
|
wget \
|
||||||
curl \
|
curl \
|
||||||
jq \
|
jq \
|
||||||
|
rclone \
|
||||||
gnupg && \
|
gnupg && \
|
||||||
pacman -Scc --noconfirm && \
|
pacman -Scc --noconfirm && \
|
||||||
rm -rf /var/cache/pacman/pkg/*
|
rm -rf /var/cache/pacman/pkg/*
|
||||||
@@ -137,8 +141,14 @@ RUN useradd -m -G wheel -s /bin/bash builder && \
|
|||||||
chmod 700 /home/builder/.gnupg && \
|
chmod 700 /home/builder/.gnupg && \
|
||||||
chown -R builder:builder /home/builder
|
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 && \
|
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).
|
# Pacman wrapper that auto-resolves package conflicts (e.g. rustup vs rust).
|
||||||
# makepkg only passes --noconfirm to pacman, which defaults to 'N' on conflict
|
# makepkg only passes --noconfirm to pacman, which defaults to 'N' on conflict
|
||||||
|
|||||||
+254
-149
@@ -1,75 +1,163 @@
|
|||||||
#!/bin/bash
|
#!/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.
|
# Unscoped edge builds exclude skip_build packages. Stable also requires the fast release ring.
|
||||||
# Explicit --package selections may build packages with skip_build=true.
|
# Explicit --package selections may build packages with skip_build=true.
|
||||||
|
|
||||||
# Setup directories
|
# Setup directories
|
||||||
ARCH=${ARCH:-x86_64}
|
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}
|
MIRROR=${MIRROR:-edge}
|
||||||
DRY_RUN=${DRY_RUN:-false}
|
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}
|
PKGBUILDS_DIR=${PKGBUILDS_DIR:-/pkgbuilds}
|
||||||
BUILD_OUTPUT_DIR=${BUILD_OUTPUT_DIR:-/build-output/$MIRROR/$ARCH}
|
BUILD_OUTPUT_DIR=${BUILD_OUTPUT_DIR:-/build-output/$MIRROR/$ARCH}
|
||||||
FINAL_OUTPUT_DIR=${FINAL_OUTPUT_DIR:-/pkgs.omarchy.org/$MIRROR/$ARCH}
|
FINAL_OUTPUT_DIR=${FINAL_OUTPUT_DIR:-/pkgs.omarchy.org/$MIRROR/$ARCH}
|
||||||
HELPERS_DIR=${HELPERS_DIR:-/helpers}
|
HELPERS_DIR=${HELPERS_DIR:-/helpers}
|
||||||
SRC_DIR=${SRC_DIR:-/src}
|
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"
|
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 [[ "$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
|
# Import GPG keys
|
||||||
/build/import-gpg-keys.sh || exit 1
|
/build/import-gpg-keys.sh || exit 1
|
||||||
|
|
||||||
mkdir -p "$BUILD_OUTPUT_DIR" "$FINAL_OUTPUT_DIR"
|
mkdir -p "$BUILD_OUTPUT_DIR" "$FINAL_OUTPUT_DIR"
|
||||||
|
|
||||||
|
# The image drops pacman's private signing key. Keyring upgrades need a
|
||||||
|
# container-local key to sign newly trusted keys.
|
||||||
|
sudo pacman-key --init || exit 1
|
||||||
|
|
||||||
# Bring the container up to date before any makedepends are installed. The
|
# Bring the container up to date before any makedepends are installed. The
|
||||||
# image is layer-cached, so its glibc drifts behind the mirror while makepkg
|
# image is layer-cached, so its glibc drifts behind the mirror while makepkg
|
||||||
# -s pulls makedepends from the freshly synced database -- a partial upgrade
|
# -s pulls makedepends from the freshly synced database -- a partial upgrade
|
||||||
# that breaks the new packages (imagemagick wanting GLIBC_2.44, etc).
|
# that breaks the new packages (imagemagick wanting GLIBC_2.44, etc).
|
||||||
# Done before the Omarchy repos are added so only core/extra participate.
|
# Done before the Omarchy repos are added so only core/extra participate.
|
||||||
echo "==> Updating build container packages..."
|
echo "==> Updating build container packages..."
|
||||||
sudo pacman -Syu --noconfirm
|
sudo pacman -Syu --noconfirm || exit 1
|
||||||
|
|
||||||
# Configure Omarchy repositories for dependency resolution
|
# Configure Omarchy repositories for dependency resolution
|
||||||
echo "==> Configuring Omarchy repositories for dependency resolution..."
|
echo "==> Configuring Omarchy repositories for dependency resolution..."
|
||||||
|
|
||||||
# Always add omarchy-build repo (for incremental builds)
|
# Always add omarchy-build repo first (for incremental builds). Repository
|
||||||
# Packages in build-output are unsigned, so use SigLevel = Never
|
# order is pacman's priority order, so this must precede the official repos;
|
||||||
sudo tee -a /etc/pacman.conf > /dev/null <<EOF
|
# otherwise pacman can select an older official package with the same name.
|
||||||
|
# Packages in build-output are unsigned, so use SigLevel = Never.
|
||||||
[omarchy-build]
|
sudo sed -i "/^\[core\]$/i [omarchy-build]\nSigLevel = Never\nServer = file://$BUILD_OUTPUT_DIR\n" /etc/pacman.conf
|
||||||
SigLevel = Never
|
|
||||||
Server = file://$BUILD_OUTPUT_DIR
|
|
||||||
EOF
|
|
||||||
echo " -> omarchy-build (priority 1): $BUILD_OUTPUT_DIR"
|
echo " -> omarchy-build (priority 1): $BUILD_OUTPUT_DIR"
|
||||||
|
|
||||||
# Initialize empty build database if it doesn't exist
|
# 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
|
if [[ ! -f "omarchy-build.db.tar.zst" ]]; then
|
||||||
# Create an empty database
|
# Create an empty database
|
||||||
repo-add omarchy-build.db.tar.zst >/dev/null 2>&1
|
repo-add omarchy-build.db.tar.zst >/dev/null 2>&1 || exit 1
|
||||||
ln -sf omarchy-build.db.tar.zst omarchy-build.db
|
ln -sf omarchy-build.db.tar.zst omarchy-build.db || exit 1
|
||||||
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
|
|
||||||
fi
|
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
|
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
|
sudo sed -i "/^\[core\]$/i [omarchy]\nSigLevel = Optional TrustAll\nServer = file://$FINAL_OUTPUT_DIR\n" /etc/pacman.conf
|
||||||
|
|
||||||
[omarchy]
|
|
||||||
SigLevel = Optional TrustAll
|
|
||||||
Server = file://$FINAL_OUTPUT_DIR
|
|
||||||
EOF
|
|
||||||
echo " -> omarchy (priority 2): $FINAL_OUTPUT_DIR"
|
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
|
fi
|
||||||
|
|
||||||
# Sync pacman database
|
# Sync pacman database
|
||||||
sudo pacman -Sy
|
sudo pacman -Sy || exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "==> Package Builder"
|
echo "==> Package Builder"
|
||||||
@@ -82,8 +170,6 @@ if [[ "$DRY_RUN" == true ]]; then
|
|||||||
echo "==> Dry run: yes (plan only; makepkg will not run)"
|
echo "==> Dry run: yes (plan only; makepkg will not run)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FAILED_PACKAGES=""
|
|
||||||
SUCCESSFUL_PACKAGES=""
|
|
||||||
SKIPPED_PACKAGES=""
|
SKIPPED_PACKAGES=""
|
||||||
|
|
||||||
# Find package directory
|
# Find package directory
|
||||||
@@ -105,10 +191,10 @@ LOCAL_VERSION_CACHE_LOADED=false
|
|||||||
LOCAL_VERSION_CACHE_DB=""
|
LOCAL_VERSION_CACHE_DB=""
|
||||||
|
|
||||||
load_local_versions() {
|
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
|
if [[ ! -f "$db" ]]; then
|
||||||
db="$FINAL_OUTPUT_DIR/omarchy.db"
|
db="$PUBLISHED_DB_DIR/omarchy.db"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -f "$db" ]] || return 0
|
[[ -f "$db" ]] || return 0
|
||||||
@@ -155,26 +241,9 @@ get_local_version() {
|
|||||||
# Returns 0 (success) if should build, 1 if should skip
|
# Returns 0 (success) if should build, 1 if should skip
|
||||||
should_build_for_arch() {
|
should_build_for_arch() {
|
||||||
local pkg="$1"
|
local pkg="$1"
|
||||||
local current_arch="$ARCH"
|
local pkgdir
|
||||||
local pkgdir=$(find_package_dir "$pkg")
|
pkgdir=$(find_package_dir "$pkg")
|
||||||
local pkgbuild="$pkgdir/PKGBUILD"
|
[[ -n "$pkgdir" ]] && package_supports_arch "$pkgdir" "$ARCH"
|
||||||
|
|
||||||
[[ ! -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# For VCS packages, makepkg recalculates pkgver() before the build. If the
|
# For VCS packages, makepkg recalculates pkgver() before the build. If the
|
||||||
@@ -224,22 +293,62 @@ refresh_vcs_pkgver_preserving_local_pkgrel() {
|
|||||||
fi
|
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 a package
|
||||||
build_package() {
|
build_package() {
|
||||||
local pkg="$1"
|
local pkg="$1"
|
||||||
local pkgdir=$(find_package_dir "$pkg")
|
local pkgdir
|
||||||
|
pkgdir=$(find_package_dir "$pkg") || return 1
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " -> Processing: $pkg"
|
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
|
# Copy to build directory
|
||||||
cd /src
|
cd /src || return 1
|
||||||
rm -rf "$pkg"
|
rm -rf "$pkg" || return 1
|
||||||
cp -r "$pkgdir" "$pkg"
|
cp -r "$pkgdir" "$pkg" || return 1
|
||||||
cd "/src/$pkg" || return 1
|
cd "/src/$pkg" || return 1
|
||||||
|
|
||||||
refresh_vcs_pkgver_preserving_local_pkgrel "$pkg" || {
|
refresh_vcs_pkgver_preserving_local_pkgrel "$pkg" || {
|
||||||
FAILED_PACKAGES="$FAILED_PACKAGES $pkg"
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +357,6 @@ build_package() {
|
|||||||
|
|
||||||
if [[ -z "$pkgbuild_version" ]]; then
|
if [[ -z "$pkgbuild_version" ]]; then
|
||||||
echo " Failed to read PKGBUILD version"
|
echo " Failed to read PKGBUILD version"
|
||||||
FAILED_PACKAGES="$FAILED_PACKAGES $pkg"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -280,37 +388,72 @@ build_package() {
|
|||||||
# Build package without signing (signing is done separately)
|
# Build package without signing (signing is done separately)
|
||||||
# PACMAN override uses a wrapper that adds --ask 4 to auto-resolve conflicts
|
# 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
|
# (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
|
# Ensure output directory exists
|
||||||
mkdir -p "$BUILD_OUTPUT_DIR"
|
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)
|
if [[ ${#package_files[@]} -eq 0 ]]; then
|
||||||
local new_pkgs=($(ls -t ${pkg}-*.pkg.tar.* 2>/dev/null | grep -v '\.sig$' | grep -v 'omarchy-build\.db'))
|
echo " Makepkg produced no package files for $pkg"
|
||||||
|
return 1
|
||||||
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
|
|
||||||
fi
|
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"
|
echo " Successfully built $pkg"
|
||||||
SUCCESSFUL_PACKAGES="$SUCCESSFUL_PACKAGES $pkg"
|
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
echo " Makepkg failed for $pkg"
|
echo " Makepkg failed for $pkg"
|
||||||
echo " DEBUG: Files in build directory:"
|
echo " DEBUG: Files in build directory:"
|
||||||
ls -lah *.pkg.tar.* 2>&1 | head -20 || echo " No package files found"
|
ls -lah *.pkg.tar.* 2>&1 | head -20 || echo " No package files found"
|
||||||
FAILED_PACKAGES="$FAILED_PACKAGES $pkg"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -325,11 +468,17 @@ get_package_deps() {
|
|||||||
return
|
return
|
||||||
fi
|
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
|
source "$pkgbuild" 2>/dev/null
|
||||||
echo "${depends[@]} ${makedepends[@]}"
|
for kind in depends makedepends checkdepends; do
|
||||||
) | tr ' ' '\n' | while read -r dep; 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')
|
# Strip version constraints (e.g., 'hyprshade>=1.0' -> 'hyprshade')
|
||||||
dep=$(echo "$dep" | sed 's/[<>=].*$//')
|
dep=$(echo "$dep" | sed 's/[<>=].*$//')
|
||||||
# Check if this dependency exists in our pkgbuilds
|
# Check if this dependency exists in our pkgbuilds
|
||||||
@@ -414,27 +563,37 @@ check_needs_build() {
|
|||||||
|
|
||||||
if [[ "$local_version" == "$pkgbuild_version" ]]; then
|
if [[ "$local_version" == "$pkgbuild_version" ]]; then
|
||||||
return 1 # Already up to date
|
return 1 # Already up to date
|
||||||
else
|
|
||||||
return 0 # Needs building
|
|
||||||
fi
|
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 that should be built for the selected mirror
|
||||||
collect_packages() {
|
collect_packages() {
|
||||||
packages_for_unscoped_build "$MIRROR"
|
packages_for_unscoped_build "$MIRROR" "$ARCH"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main execution
|
# Main execution
|
||||||
if [[ "$DRY_RUN" != true ]]; then
|
if [[ "$DRY_RUN" != true ]]; then
|
||||||
cd "$SRC_DIR"
|
cd "$SRC_DIR" || exit 1
|
||||||
|
build_package "$BUILD_PACKAGE"
|
||||||
|
exit $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TOTAL_COUNT=0
|
|
||||||
|
|
||||||
echo "==> Checking which packages need building..."
|
echo "==> Checking which packages need building..."
|
||||||
|
|
||||||
# First pass: determine which packages need building
|
# First pass: determine which packages need building
|
||||||
PACKAGES_TO_BUILD=()
|
PACKAGES_TO_BUILD=()
|
||||||
|
ORDERED_PACKAGES=()
|
||||||
|
PLANNED_DEPENDENCIES=()
|
||||||
|
|
||||||
# If PACKAGES is specified, only check those packages
|
# If PACKAGES is specified, only check those packages
|
||||||
if [[ -n "$PACKAGES" ]]; then
|
if [[ -n "$PACKAGES" ]]; then
|
||||||
@@ -473,13 +632,6 @@ if [[ -n "$PACKAGES" ]]; then
|
|||||||
else
|
else
|
||||||
# Build all packages that need updates from the relevant directories
|
# Build all packages that need updates from the relevant directories
|
||||||
while IFS= read -r pkg; do
|
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
|
if check_needs_build "$pkg"; then
|
||||||
PACKAGES_TO_BUILD+=("$pkg")
|
PACKAGES_TO_BUILD+=("$pkg")
|
||||||
else
|
else
|
||||||
@@ -492,7 +644,7 @@ fi
|
|||||||
if [[ ${#PACKAGES_TO_BUILD[@]} -eq 0 ]]; then
|
if [[ ${#PACKAGES_TO_BUILD[@]} -eq 0 ]]; then
|
||||||
echo "==> All packages are up to date!"
|
echo "==> All packages are up to date!"
|
||||||
else
|
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..."
|
echo "==> Determining build order based on dependencies..."
|
||||||
|
|
||||||
# Second pass: order only the packages that need building
|
# Second pass: order only the packages that need building
|
||||||
@@ -515,6 +667,7 @@ else
|
|||||||
((unmet_deps_count[$pkg]++))
|
((unmet_deps_count[$pkg]++))
|
||||||
# Track that dep blocks pkg from building
|
# Track that dep blocks pkg from building
|
||||||
blocks_packages[$dep]="${blocks_packages[$dep]} $pkg"
|
blocks_packages[$dep]="${blocks_packages[$dep]} $pkg"
|
||||||
|
PLANNED_DEPENDENCIES+=("$pkg $dep")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
done < <(get_package_deps "$pkg")
|
done < <(get_package_deps "$pkg")
|
||||||
@@ -529,7 +682,6 @@ else
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Build packages as dependencies become available
|
# Build packages as dependencies become available
|
||||||
ORDERED_PACKAGES=()
|
|
||||||
while [[ ${#ready_to_build[@]} -gt 0 ]]; do
|
while [[ ${#ready_to_build[@]} -gt 0 ]]; do
|
||||||
# Take the first ready package
|
# Take the first ready package
|
||||||
current="${ready_to_build[0]}"
|
current="${ready_to_build[0]}"
|
||||||
@@ -551,63 +703,16 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "==> Build order: ${ORDERED_PACKAGES[@]}"
|
echo "==> Build order: ${ORDERED_PACKAGES[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == true ]]; then
|
# The host consumes plain data, never shell code or parsed human log output.
|
||||||
echo ""
|
if [[ -n "$BUILD_PLAN_DIR" ]]; then
|
||||||
echo "==> Dry run complete. Packages that would build: ${ORDERED_PACKAGES[@]}"
|
mkdir -p "$BUILD_PLAN_DIR" || exit 1
|
||||||
exit 0
|
printf '%s\n' "${ORDERED_PACKAGES[@]}" | sed '/^$/d' > "$BUILD_PLAN_DIR/packages" || exit 1
|
||||||
fi
|
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
|
||||||
# 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
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "========================================"
|
echo "==> Plan complete. Packages that would build: ${ORDERED_PACKAGES[*]}"
|
||||||
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!"
|
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
# The x86_64 builder pool
|
||||||
|
|
||||||
|
x86_64 package builds run on DigitalOcean droplets that exist for one job
|
||||||
|
each. aarch64 builds run on GitHub's `ubuntu-24.04-arm` runners and need
|
||||||
|
nothing here.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
- **Jobs ask for a runner labelled `omarchy-builder`.** `build-pr.yml` builds
|
||||||
|
x86_64 packages there, and so does the `rebuild` job in `publish.yml`.
|
||||||
|
- **A controller creates one droplet per queued job.** `controller.sh` runs
|
||||||
|
from a systemd timer every minute on a small always-on droplet (tag
|
||||||
|
`omarchy-controller`). It polls GitHub for queued jobs with the label and
|
||||||
|
creates droplets up to `MAX_DROPLETS`. It has no inbound endpoint.
|
||||||
|
- **A droplet runs one job and powers off.** `runner-cloud-init.yaml` installs
|
||||||
|
Docker and the GitHub runner, registers it `--ephemeral` with a
|
||||||
|
registration token that expires in an hour, runs the job, and powers off, also when the runner download or
|
||||||
|
registration fails. The controller removes any other failed droplet at
|
||||||
|
`MAX_AGE_MINUTES`.
|
||||||
|
- **The controller deletes droplets** that are powered off, older than
|
||||||
|
`MAX_AGE_MINUTES`, or still provisioning after `MAX_BOOT_MINUTES`, and
|
||||||
|
replaces a stuck one in the same tick.
|
||||||
|
- **Sizes and regions fall back.** Each tick tries every size in `SIZES`, in
|
||||||
|
every region DigitalOcean lists it in stock. `REGIONS` only sets which
|
||||||
|
regions go first. A refused create is logged with DigitalOcean's message.
|
||||||
|
|
||||||
|
No secret reaches a builder. Signing and upload happen in the `publish` job on
|
||||||
|
a GitHub-hosted runner.
|
||||||
|
|
||||||
|
## Operate it
|
||||||
|
|
||||||
|
SSH to the controller droplet as root. It is the droplet tagged
|
||||||
|
`omarchy-controller` in the DigitalOcean account that pays for the builders;
|
||||||
|
the operators' keys were installed when it was created.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u omarchy-controller -n 50 --no-pager # What it is doing
|
||||||
|
journalctl -u omarchy-controller -f # Follow it
|
||||||
|
systemctl list-timers omarchy-controller.timer # Is it ticking
|
||||||
|
```
|
||||||
|
|
||||||
|
| Log line | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `creating <name> (<size> in <region>)` | A queued job is getting a droplet |
|
||||||
|
| `<size> in <region> refused: <message>` | DigitalOcean refused; the next region or size is tried |
|
||||||
|
| `no size in '<sizes>' can be created in any region` | Every create was refused. Read the `refused:` lines above it for why: out of stock (add a size to `SIZES`), or the account's droplet limit |
|
||||||
|
| `at cap (<live>/<max>, <busy> busy) with <queued> queued` | `MAX_DROPLETS` is reached. Raise it if the queue is long |
|
||||||
|
| `deleting droplet <id> (status=<status> age=<n>m)` | Reaping a finished, old or stuck droplet |
|
||||||
|
|
||||||
|
Check the pool from anywhere:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh api repos/omacom/omarchy-pkgs/actions/runners --jq \
|
||||||
|
'"online \([.runners[]|select(.status=="online")]|length), busy \([.runners[]|select(.busy)]|length)"'
|
||||||
|
```
|
||||||
|
|
||||||
|
Jobs queued with no runner online for several minutes means the controller is
|
||||||
|
not creating droplets: read its journal. See what is waiting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh run list -R omacom/omarchy-pkgs --workflow build-pr.yml --status queued
|
||||||
|
```
|
||||||
|
|
||||||
|
Only x86_64 jobs use this pool. A queued aarch64 job is waiting on GitHub's
|
||||||
|
own runners.
|
||||||
|
|
||||||
|
### Change settings
|
||||||
|
|
||||||
|
Edit `/etc/omarchy-controller.env` on the box. The next tick reads it.
|
||||||
|
|
||||||
|
| Setting | Default in `controller-box/controller.env.example` | |
|
||||||
|
|---|---|---|
|
||||||
|
| `MAX_DROPLETS` | 6 | Builders alive at once. The DigitalOcean account's droplet limit also applies |
|
||||||
|
| `SIZES` | `g5-32vcpu-64gb-50gb g5-32vcpu-128gb-50gb` | Tried in order |
|
||||||
|
| `REGIONS` | empty | Regions to try first; empty means any |
|
||||||
|
| `MAX_AGE_MINUTES` | 200 | Delete a droplet older than this |
|
||||||
|
| `MAX_BOOT_MINUTES` | 10 | Delete a droplet still provisioning after this |
|
||||||
|
| `DO_SSH_KEYS` | Ryan, DHH and Emir's account key IDs | Attached to every builder. Without one, DigitalOcean emails a root password per builder |
|
||||||
|
|
||||||
|
### Change the controller
|
||||||
|
|
||||||
|
Merge the change to `master`. The unit pulls `/opt/omarchy-pkgs` before every
|
||||||
|
tick, so it is live within a minute. The unit and timer files are copies made
|
||||||
|
when the box was created; after changing them, on the box:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp /opt/omarchy-pkgs/ci/controller-box/omarchy-controller.{service,timer} /etc/systemd/system/
|
||||||
|
systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
`tests/controller.sh` exercises every controller decision against canned API
|
||||||
|
responses. Run it before merging a change.
|
||||||
|
|
||||||
|
### Reach a builder
|
||||||
|
|
||||||
|
Operators can SSH to a builder as root while it lives. It powers off after its
|
||||||
|
one job.
|
||||||
|
|
||||||
|
### Clean up by hand
|
||||||
|
|
||||||
|
The controller reaps on its own. To do it manually, delete droplets tagged
|
||||||
|
`omarchy-builder` in the DigitalOcean account.
|
||||||
|
|
||||||
|
## Stand up a controller
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DIGITALOCEAN_TOKEN=<account that pays for builders> GITHUB_TOKEN=<fine-grained PAT> \
|
||||||
|
REPO=omacom/omarchy-pkgs ci/controller-box/create.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- The GitHub PAT is fine-grained and scoped to this repository: Actions read,
|
||||||
|
Administration read and write (for runner registration tokens).
|
||||||
|
- The DigitalOcean token is written into the box's env file, so it is the
|
||||||
|
account that pays. It needs ssh_key:read to attach `DO_SSH_KEYS`; without
|
||||||
|
it every create is refused with 403.
|
||||||
|
- `ADMIN_GITHUB_USERS` names whose GitHub SSH keys get root on the box and
|
||||||
|
the builders. Set it; the default is a fixed list in `create.sh`.
|
||||||
|
- The script refuses to create a second controller.
|
||||||
@@ -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,24 @@
|
|||||||
|
# /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=[]
|
||||||
|
# DigitalOcean account SSH key IDs attached to every builder (JSON array), so
|
||||||
|
# DigitalOcean does not email a root password for each one. Needs ssh_key:read
|
||||||
|
# on DIGITALOCEAN_TOKEN. List them with: doctl compute ssh-key list
|
||||||
|
# Ryan R. Hughes, DHH, Emir.
|
||||||
|
DO_SSH_KEYS=[59143572,59143588,59779105]
|
||||||
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 emirb}
|
||||||
|
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
+201
@@ -0,0 +1,201 @@
|
|||||||
|
#!/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:-[]}
|
||||||
|
# DigitalOcean account SSH key IDs or fingerprints attached at creation (JSON
|
||||||
|
# array). A droplet created with none gets a root password that DigitalOcean
|
||||||
|
# emails to the token's owner, once per builder. Attaching one needs the
|
||||||
|
# ssh_key:read scope on DIGITALOCEAN_TOKEN; without it every create is
|
||||||
|
# refused with 403.
|
||||||
|
DO_SSH_KEYS=${DO_SSH_KEYS:-[]}
|
||||||
|
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" --argjson keys "$DO_SSH_KEYS" \
|
||||||
|
'{name:$name, region:$region, size:$size, image:$image, tags:[$tag], user_data:$ud, ssh_keys:$keys, 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,86 @@
|
|||||||
|
#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__).
|
||||||
|
# Never by password: DigitalOcean sets and emails one when no account key is
|
||||||
|
# attached, and its vendor data turns password logins on to match.
|
||||||
|
disable_root: false
|
||||||
|
ssh_pwauth: 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,80 @@
|
|||||||
|
# How a package builds
|
||||||
|
|
||||||
|
`bin/build` is the one builder: PR builds, rebuilds at merge and local builds
|
||||||
|
all call it.
|
||||||
|
|
||||||
|
## Dependency resolution
|
||||||
|
|
||||||
|
Within one `bin/build` run, the build system handles dependencies between the
|
||||||
|
packages it was asked to build. CI builds each package in its own job, so a
|
||||||
|
PR's packages do not see each other's fresh builds; they resolve from `edge`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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. **One-time setup when the image package is first
|
||||||
|
created:** 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.
|
||||||
|
|
||||||
|
`publish.yml` and `unpublish.yml` pull the compatible image to sign and update
|
||||||
|
databases. Package build jobs still build their own image on the runner.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Rebuild triggers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/sync-rebuilds # Bump every package whose dependencies moved
|
||||||
|
bin/sync-rebuilds quickshell-git # Update specific packages
|
||||||
|
bin/sync-rebuilds --self-test # Run the regression tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Some packages have to be rebuilt when something they link against changes, even though nothing in their own source moved. A Qt private-API consumer is the usual case: `Qt_6_PRIVATE_API` symbols are not covered by the soname, so a qt6-base point release can leave an installed binary unable to resolve a symbol at startup, and pacman upgrades Qt out from under it because the dependency is unversioned. The package still builds from the same git commit, so its version alone would not trigger a rebuild.
|
||||||
|
|
||||||
|
A package names those dependencies in `.omarchy/package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "source": "local", "rebuild_on": ["qt6-base", "qt6-declarative", "qt6-wayland"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
`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 built for
|
||||||
|
each architecture by the PR that carries the bump.
|
||||||
|
|
||||||
|
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 rebuilding without a version change would ship nothing. Bumping pkgrel needs no other change: the PR that carries the bump builds the package, and the merge publishes it.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## The workflow
|
||||||
|
|
||||||
|
`sync-rebuilds.yml` runs `bin/sync-rebuilds` every 6 hours and opens one PR per
|
||||||
|
package on `auto/sync-rebuilds-<package>` with auto-merge enabled. Each PR lands
|
||||||
|
once `result`, `self-tests` and `build-isolation` pass, and the merge publishes.
|
||||||
|
A rebuild that fails stays an open red PR for a maintainer without holding back
|
||||||
|
the others.
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Cutting an Omarchy release
|
||||||
|
|
||||||
|
The release train runs from `bin/omarchy-release`. Nothing in the GitHub
|
||||||
|
workflows promotes a channel: its channel moves and RC builds were written to
|
||||||
|
execute on the old repository host, which is abandoned (see
|
||||||
|
[repository-host.md](repository-host.md)), against a local copy of the
|
||||||
|
published tree.
|
||||||
|
|
||||||
|
> `rc` and `ship` build the release pair with `bin/repo release` and move
|
||||||
|
> channels with `bin/repo advance`, on the machine that holds the published
|
||||||
|
> tree. No workflow replaces them yet. That machine's tree has to match the
|
||||||
|
> bucket first: `advance` reads the source channel from local disk.
|
||||||
|
|
||||||
|
`bin/omarchy-release` is the front door. A release train has three human
|
||||||
|
moments, each one command — and bare `omarchy-release` observes reality
|
||||||
|
(branches, pins, published channels, tags) and walks you to the next one:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/omarchy-release # shepherd: status + guided next step
|
||||||
|
bin/omarchy-release start 4.0.2 # open the train: branch v4-0-2 + staging PR
|
||||||
|
bin/omarchy-release pick # choose merged PRs to cherry-pick (multi-select)
|
||||||
|
bin/omarchy-release rc # publish the next 4.0.2rcN to the rc channel
|
||||||
|
bin/omarchy-release ship # tag, final pins, promote rc → stable,
|
||||||
|
# draft GitHub release, ISO, website — one swoop
|
||||||
|
bin/omarchy-release doctor # verify credentials/connections up front
|
||||||
|
```
|
||||||
|
|
||||||
|
Versions are inferred from branch names (`v4-0-2` ⇒ `4.0.2rcN` ⇒ tag
|
||||||
|
`v4.0.2`); `start` is the only place a version is typed. Every command is
|
||||||
|
idempotent — re-runs skip whatever is already done. `ship` refuses to promote
|
||||||
|
a commit no RC was cut from. One exception: `ship` carries on past a failed
|
||||||
|
ISO or website step, and a later `ship` exits once stable holds the version
|
||||||
|
and the GitHub release exists. Finish a failed ISO or website step by hand.
|
||||||
|
|
||||||
|
## Promoting a channel
|
||||||
|
|
||||||
|
Ship the tested rc channel to stable (copies packages + signatures, then
|
||||||
|
cleans, rebuilds the database, and syncs):
|
||||||
|
|
||||||
|
```
|
||||||
|
bin/repo advance --from rc --to stable --arch all
|
||||||
|
```
|
||||||
|
|
||||||
|
Open a release train that ships new edge packages by carrying edge into rc
|
||||||
|
first:
|
||||||
|
|
||||||
|
```
|
||||||
|
bin/repo advance --from edge --to rc --arch all
|
||||||
|
```
|
||||||
|
|
||||||
|
`advance` moves x86_64 alone unless given `--arch`. `--arch all` covers the
|
||||||
|
architectures in `PUBLISHED_ARCHES` (`helpers/paths.sh`), x86_64 only by
|
||||||
|
default; set `OMARCHY_ARCHES="x86_64 aarch64"` on the machine that runs it to
|
||||||
|
promote both. The variable is not carried over when `bin/repo` forwards a
|
||||||
|
command over ssh, and `omarchy-release ship` promotes through the same path. It refuses to
|
||||||
|
carry a package into a channel its `channels` metadata excludes.
|
||||||
|
|
||||||
|
## The pin engine
|
||||||
|
|
||||||
|
The release pair is marked `"pinned": true`. It is built for `rc` only from
|
||||||
|
the `rc` branch worktree, with `OMARCHY_RC_PINS=1` set (`omarchy-release rc`
|
||||||
|
sets it), and never built for `stable`: stable gets it by promotion. A merge to
|
||||||
|
`master` therefore cannot overwrite an RC in flight.
|
||||||
|
|
||||||
|
The `omarchy` and `omarchy-settings` packages are released as a pair, always
|
||||||
|
built from the same upstream commit of basecamp/omarchy.
|
||||||
|
|
||||||
|
**Use `bin/omarchy-release`** — it drives the whole train across all four repositories and
|
||||||
|
calls the pin engine below for you.
|
||||||
|
|
||||||
|
`bin/omarchy-pkgs` is that engine, available directly for one-off pins and
|
||||||
|
debugging. It rewrites both PKGBUILDs in lockstep (same `_tag`/`_commit`/
|
||||||
|
`pkgver`/`sha256sums`), validates ordering with `vercmp`, commits, and pushes
|
||||||
|
the current branch. Driven by `omarchy-release` it pins to the release branch
|
||||||
|
on the `rc` branch and orders against the rc channel; invoked directly it
|
||||||
|
targets the current branch and edge.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/omarchy-pkgs release v4.0.0 # Final release from the upstream v4.0.0 tag
|
||||||
|
bin/omarchy-pkgs release rc v4.0.0 # Newest upstream v4.0.0-rcN tag -> 4.0.0rcN
|
||||||
|
bin/omarchy-pkgs release beta v4.0.0 # Same for beta (alpha also supported)
|
||||||
|
bin/omarchy-pkgs release latest # Newest upstream tag, rc/beta included (prompts)
|
||||||
|
bin/omarchy-pkgs release rc # Untagged RC from the quattro tip, auto-numbered
|
||||||
|
bin/omarchy-pkgs release --commit abc123 --base 4.1.0 # Untagged RC from a commit
|
||||||
|
bin/omarchy-pkgs release ... --dry-run # Show the plan; write nothing
|
||||||
|
bin/omarchy-pkgs release ... --no-push # Full flow, local commit only (testing)
|
||||||
|
bin/omarchy-pkgs self-test # Version normalization + ordering tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning rules
|
||||||
|
|
||||||
|
- Finals are `X.Y.Z`; pre-releases are `X.Y.ZalphaN` / `X.Y.ZbetaN` /
|
||||||
|
`X.Y.ZrcN` in the **attached** form only. pacman's vercmp orders
|
||||||
|
`4.0.0alpha1 < 4.0.0beta1 < 4.0.0rc1 < 4.0.0`, but separator forms
|
||||||
|
(`4.0.0.rc1`, `4.0.0_rc1`) sort **after** `4.0.0` and would strand users on
|
||||||
|
the pre-release — the tooling normalizes upstream tags (`v4.0.0-rc1`,
|
||||||
|
`v4.0.0-rc.1`, ...) to the attached form and refuses anything it cannot
|
||||||
|
normalize. Upstream tags are cut on the quattro branch.
|
||||||
|
- `pkgrel` resets to 1 on every version change. Bump `pkgrel` by hand only to
|
||||||
|
repackage the same source.
|
||||||
|
- `epoch` is never set by tooling. It is sticky forever; adding one is a
|
||||||
|
human decision of last resort.
|
||||||
|
|
||||||
|
## Where releases land
|
||||||
|
|
||||||
|
- **RCs build for the rc channel.** RC testers and RC ISO installs run the real
|
||||||
|
`omarchy` package against the package set stable users will actually get.
|
||||||
|
Stable never sees an rc version; rc testers upgrade rc1 → rc2 → final
|
||||||
|
naturally.
|
||||||
|
- **Finals build into rc, then the whole channel promotes.** After testing, the
|
||||||
|
ship step promotes the exact tested artifacts (packages and signatures)
|
||||||
|
forward:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo advance --from rc --to stable --arch all
|
||||||
|
```
|
||||||
|
|
||||||
|
Neither package is on the `fast` ring, and `bin/omarchy-pkgs` never touches
|
||||||
|
stable — promotion is always this explicit step.
|
||||||
@@ -0,0 +1,586 @@
|
|||||||
|
# The repository host (abandoned)
|
||||||
|
|
||||||
|
**This pipeline is abandoned.** Packages are built and published by GitHub
|
||||||
|
workflows: see the [README](../README.md). Don't follow this page to publish
|
||||||
|
anything.
|
||||||
|
|
||||||
|
The package pipeline used to run on one machine that built, signed and
|
||||||
|
published every channel on a timer. This page is kept as the reference for
|
||||||
|
the tooling that was written for it and is still in the tree: `bin/repo`, the
|
||||||
|
units in `systemd/`, and the host side of the release train
|
||||||
|
([releases.md](releases.md)).
|
||||||
|
|
||||||
|
**That tooling writes to the same bucket as CI.** If you run any of it
|
||||||
|
(`release`, `sync`, `promote`, `push`, `deploy`, `advance`, `remove`), these
|
||||||
|
rules apply:
|
||||||
|
|
||||||
|
- Do not run `bin/repo release`, `sync`, `push` or `deploy` for a package CI
|
||||||
|
publishes. Merge a PR, or dispatch `publish.yml`, instead.
|
||||||
|
- Keep the `omarchy-auto-release-*` and `omarchy-check-versions` timers
|
||||||
|
disabled while CI publishes the same channels. Two writers on one channel
|
||||||
|
database overwrite each other.
|
||||||
|
- Never pass `--prune` to `bin/repo sync`. The host's tree does not hold what
|
||||||
|
CI published, and `--prune` deletes remote files the local tree lacks.
|
||||||
|
- Never rebuild a version that is already published. Bump `pkgrel`.
|
||||||
|
|
||||||
|
## Setting up a host
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
### aarch64 under 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
|
||||||
|
# Verify
|
||||||
|
podman run --rm --platform linux/arm64 docker.io/library/alpine:latest uname -m
|
||||||
|
# Should output: aarch64
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: emulated builds are much slower than native ones.
|
||||||
|
|
||||||
|
## Published architectures
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
`helpers/paths.sh` names the architectures the host's scheduled pipeline and
|
||||||
|
release train work on. CI does not read it:
|
||||||
|
|
||||||
|
```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. Rerunning the same advance after a failed remote sync 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. CI
|
||||||
|
does not read this list: it builds the architectures in `CI_ARCHES`, both by
|
||||||
|
default. 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.
|
||||||
|
|
||||||
|
## Release in one command
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Stable Mirror
|
||||||
|
bin/repo release --mirror stable
|
||||||
|
|
||||||
|
# ARM64
|
||||||
|
bin/repo release --arch aarch64
|
||||||
|
|
||||||
|
# Build one package (still skipped when its version is already published)
|
||||||
|
bin/repo release --package omarchy-nvim
|
||||||
|
|
||||||
|
# Show what would build without signing/promoting/syncing
|
||||||
|
bin/repo release --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step-by-Step
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo build # Build (unsigned)
|
||||||
|
bin/repo sign # Sign packages
|
||||||
|
bin/repo promote # Copy to production
|
||||||
|
bin/repo clean # Remove old versions
|
||||||
|
bin/repo update # Update database
|
||||||
|
bin/repo sync # Sync to remote
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building Heavy Packages Locally
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
Large packages build faster on a local machine than on the server. Build them
|
||||||
|
here, then hand the artifacts to the repository host, which signs and publishes
|
||||||
|
them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo deploy --package nvidia-580xx-utils # Build here, publish from the host
|
||||||
|
```
|
||||||
|
|
||||||
|
`deploy` is `build` followed by `push`. The two steps are also available
|
||||||
|
separately when a build needs inspecting before it ships:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/build --package nvidia-580xx-utils # Build on the fast machine
|
||||||
|
bin/repo push --package nvidia-580xx-utils # Upload + publish on the host
|
||||||
|
```
|
||||||
|
|
||||||
|
`push` uploads to the host's `build-output/`, verifies checksums, and runs
|
||||||
|
`bin/upload-prebuilt` there. Do not publish from a local checkout instead: a
|
||||||
|
local machine holds neither the complete repository nor a signing key.
|
||||||
|
|
||||||
|
**Name the package.** `build` plans against the local repository database, or
|
||||||
|
against the public channel's when there is none. `deploy` refuses to run
|
||||||
|
unscoped when the local database is missing. Unscoped builds belong on
|
||||||
|
the repository host, where `bin/repo release` does the same job against a real
|
||||||
|
database.
|
||||||
|
|
||||||
|
Every other `bin/repo` command — `sign`, `promote`, `update`, `clean`,
|
||||||
|
`advance`, `remove`, `sync`, `release` — works on the published tree. Run them
|
||||||
|
from anywhere: with a repository host configured `bin/repo` forwards them over
|
||||||
|
ssh to run there (see [Build trigger and the build host](#build-trigger-and-the-build-host)),
|
||||||
|
and `--local` forces execution on the current machine. `bin/repo build`
|
||||||
|
forwards too; call `bin/build` directly, or pass `--local`, to build on this
|
||||||
|
machine. `push` and `deploy` always run locally.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
### Global Flags
|
||||||
|
|
||||||
|
These flags can be used with all commands:
|
||||||
|
|
||||||
|
- `--mirror <edge|rc|stable>`: Selects the channel (default: `edge`).
|
||||||
|
- `--local`: Run on this machine instead of forwarding to the repository host.
|
||||||
|
- `--arch <x86_64|aarch64>`: Selects the target architecture (default: `x86_64`).
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo build # All packages (x86_64, edge)
|
||||||
|
bin/repo build --arch aarch64 # ARM64
|
||||||
|
bin/repo build --mirror rc # Release-candidate channel
|
||||||
|
bin/repo build --mirror stable # Stable channel
|
||||||
|
bin/repo build --package yay cursor-bin # Specific packages
|
||||||
|
bin/repo build --dry-run # Show what would build
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output**: Unsigned `.pkg.tar.zst` in `build-output/`. Only builds packages whose version differs from what the repository holds.
|
||||||
|
|
||||||
|
Use `--dry-run` to show the build plan without running `makepkg`.
|
||||||
|
|
||||||
|
### Sign
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo sign
|
||||||
|
```
|
||||||
|
|
||||||
|
Reads the GPG key and passphrase from the environment and signs all packages in `build-output/`. Supports `--arch` and `--mirror`.
|
||||||
|
|
||||||
|
### Promote
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo promote # Copy to production
|
||||||
|
bin/repo promote --arch aarch64 # ARM64
|
||||||
|
bin/repo promote --dry-run # Preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Copies signed packages from `build-output/` → `pkgs.omarchy.org/`.
|
||||||
|
|
||||||
|
### Clean
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo clean # Keep 2 versions
|
||||||
|
bin/repo clean --keep 3 # Keep 3 versions
|
||||||
|
bin/repo clean --dry-run # Preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Removes old package versions from the file system. **Does not update the database.**
|
||||||
|
|
||||||
|
### Update
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo update # Update database
|
||||||
|
```
|
||||||
|
|
||||||
|
Updates the repository database (adding the newest version of each package). Run this after `promote` or `clean`.
|
||||||
|
|
||||||
|
### Sync Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo sync # Sync current arch/mirror
|
||||||
|
bin/repo sync --mirror stable # Sync stable
|
||||||
|
bin/repo sync --arch aarch64 # Sync ARM64
|
||||||
|
bin/repo sync --skip-prod-check # No confirmation
|
||||||
|
```
|
||||||
|
|
||||||
|
Syncs package repositories to the remote server using rclone based on the configured mirror and architecture.
|
||||||
|
|
||||||
|
**Uploads are additive.** A local tree is not authoritative about what belongs on
|
||||||
|
the remote — `pkgs.omarchy.org/` is gitignored, and packages built on another
|
||||||
|
machine exist only there — so sync never deletes by default. To take a package
|
||||||
|
out of the channels, use the `unpublish.yml` workflow.
|
||||||
|
|
||||||
|
For the same reason sync refuses to publish a repository database built from a
|
||||||
|
tree holding fewer packages than the remote database already lists. The database
|
||||||
|
is what pacman resolves against, so a partial one hides every package it does not
|
||||||
|
know about even though the files are still on the mirror. Use `bin/repo push` to
|
||||||
|
publish packages built on another machine.
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo deploy --package nvidia-580xx-utils # Build locally, publish from the host
|
||||||
|
bin/repo deploy --host root@example.com # Point at a specific repo host
|
||||||
|
bin/repo deploy --dry-run # Show the plan, change nothing
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs `build` then `push` in one command. The repository host is resolved before
|
||||||
|
the build starts, so a missing `--host` fails immediately rather than after a long
|
||||||
|
compile.
|
||||||
|
|
||||||
|
### Push to the Repository Host
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo push # Push everything in build-output
|
||||||
|
bin/repo push --package nvidia-580xx-utils # Push one package
|
||||||
|
bin/repo push --mirror stable --arch aarch64 # Pick mirror and architecture
|
||||||
|
bin/repo push --host root@example.com # Override the repo host
|
||||||
|
bin/repo push --dry-run # Show the plan, transfer nothing
|
||||||
|
```
|
||||||
|
|
||||||
|
Uploads packages from `build-output/` to the repository host and publishes them there
|
||||||
|
with `bin/upload-prebuilt` (sign → promote → update → sync). Use it when a package
|
||||||
|
is quicker to build on a local machine than on the server.
|
||||||
|
|
||||||
|
Publishing happens on the host rather than locally for two reasons: the host
|
||||||
|
has a GPG signing key (CI has its own copy in the `publish` environment), and
|
||||||
|
it holds the repository tree that its database and sync are built from. Local
|
||||||
|
machines therefore need no secrets.
|
||||||
|
|
||||||
|
The host comes from `--host`, `$OMARCHY_REPO_HOST`, then `.repo-host`. The
|
||||||
|
setting is named for the repository rather than for building, which happens
|
||||||
|
wherever you like. The same setting tells `bin/omarchy-pkgs release` which host
|
||||||
|
to poke after a release push.
|
||||||
|
|
||||||
|
`--package` means the same thing as it does to `build`: a pkgbase, whose every
|
||||||
|
output ships together. Pushing `nvidia-580xx-utils` carries `nvidia-580xx-dkms`
|
||||||
|
and `opencl-nvidia-580xx` with it, because that is what the build produced. An
|
||||||
|
output's own name still selects just that one, for publishing a single package
|
||||||
|
on purpose. Omit `--package` to push everything built.
|
||||||
|
|
||||||
|
Publishing signs and promotes everything staged on the host, not just what this
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Other host commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo advance --from rc --to stable # Ship the tested rc channel to stable
|
||||||
|
bin/repo advance --from edge --to rc # Open a train: carry edge forward into rc
|
||||||
|
bin/repo advance --from edge --to rc --package x # Deliberate single-package advance
|
||||||
|
bin/repo advance --from rc --to stable --dry-run # Preview without changing files
|
||||||
|
bin/repo bootstrap-rc # One-time initial rc seed from stable
|
||||||
|
bin/repo timers # Timer schedule, last runs, queues, backoff, lock
|
||||||
|
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/repo remove <package> # Remove package (host workflow; CI uses unpublish.yml)
|
||||||
|
bin/clean-docker # Clear Docker images/cache (forces fresh rebuild)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build trigger and the build host
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
Release commands run from anywhere. `bin/repo` is a **remote control**: with a
|
||||||
|
repository host configured, every command that operates on the published tree
|
||||||
|
(`release`, `build`, `sign`, `promote`, `update`, `clean`, `advance`,
|
||||||
|
`bootstrap-rc`, `remove`, `sync`, `migrate`) executes ON the host over ssh —
|
||||||
|
the exact same code, run where the tree lives, so ssh'ing in and running the
|
||||||
|
same commands by hand behaves identically. Pass `--local` to force execution
|
||||||
|
on the current machine. `list`, `push`, `deploy`, and `setup` never forward
|
||||||
|
(`push`/`deploy` exist precisely to move local builds *to* the host).
|
||||||
|
|
||||||
|
Without a configured host, commands run locally — which on the build host
|
||||||
|
itself (no `.repo-host` there) is exactly right, and elsewhere the exact
|
||||||
|
commands to run are printed by the orchestrator, with the 5-minute
|
||||||
|
auto-release timer as the backstop.
|
||||||
|
|
||||||
|
The host setting is any destination `ssh` accepts, resolved in this order:
|
||||||
|
|
||||||
|
1. `--host <dest>`, for `push`, `deploy` and `bin/omarchy-release`
|
||||||
|
2. `OMARCHY_REPO_HOST` environment variable
|
||||||
|
3. the git-ignored `.repo-host` file (one line; `#` comments allowed)
|
||||||
|
|
||||||
|
The server layout lives under `/root`, so the value is `root@<ip>`,
|
||||||
|
`root@<hostname>`, or — nicest — a `Host` alias from `~/.ssh/config` that
|
||||||
|
carries the user, key, and port:
|
||||||
|
|
||||||
|
```
|
||||||
|
# .repo-host
|
||||||
|
root@pkgs.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
All connections are plain ssh; nothing else is used to reach the host.
|
||||||
|
|
||||||
|
**How "this machine is the build host" is detected — and its one caveat.**
|
||||||
|
The marker is the published database living in this checkout
|
||||||
|
(`pkgs.omarchy.org/<channel>/<arch>/omarchy.db*`), the same convention the
|
||||||
|
rest of `bin/` uses. A workstation that once ran a full local
|
||||||
|
`bin/repo release` carries that marker too and would infer it is the host.
|
||||||
|
An explicitly configured destination always outranks the inference — `--host`,
|
||||||
|
`OMARCHY_REPO_HOST`, and `.repo-host` are checked first, and only when none is
|
||||||
|
set does local detection apply. So if a machine ever misidentifies itself,
|
||||||
|
writing the real host into `.repo-host` is the fix.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
```
|
||||||
|
omarchy-pkgs/
|
||||||
|
├── pkgbuilds/ # Source PKGBUILDs
|
||||||
|
│ └── package-name/
|
||||||
|
│ ├── PKGBUILD
|
||||||
|
│ └── .omarchy/
|
||||||
|
│ ├── package.json # Source/sync/release metadata
|
||||||
|
│ └── upstream.sh # Optional custom vendor release feed hook
|
||||||
|
├── build/
|
||||||
|
├── build-output/ # Unsigned packages (temporary)
|
||||||
|
│ ├── edge/ # (rc/ and stable/ alongside, each x86_64 + aarch64)
|
||||||
|
│ ├── rc/
|
||||||
|
│ └── stable/
|
||||||
|
├── pkgs.omarchy.org/ # Signed packages (production)
|
||||||
|
│ ├── .release.lock # Host-wide lock: one channel mutation at a time
|
||||||
|
│ ├── edge/ # Each channel: x86_64/ and aarch64/
|
||||||
|
│ ├── rc/
|
||||||
|
│ └── stable/
|
||||||
|
└── bin/ # CLI tools (on host)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture-Specific Notes
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
### x86_64
|
||||||
|
- Native builds (fast)
|
||||||
|
- Mirrors: mirror.omarchy.org, rackspace, pkgbuild.com
|
||||||
|
|
||||||
|
### aarch64
|
||||||
|
- 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`; 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build x86_64
|
||||||
|
bin/repo release --package myapp
|
||||||
|
|
||||||
|
# Build aarch64
|
||||||
|
bin/repo release --arch aarch64 --package myapp
|
||||||
|
|
||||||
|
# Sync both
|
||||||
|
bin/repo sync
|
||||||
|
bin/repo sync --arch aarch64
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Management
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
Packages are only rebuilt if:
|
||||||
|
- PKGBUILD version differs from the repository version
|
||||||
|
- Package doesn't exist in production
|
||||||
|
|
||||||
|
A package that has to be rebuilt because something underneath it changed is handled by turning that into a version change: `bin/sync-rebuilds` bumps pkgrel when a dependency named in `rebuild_on` moves.
|
||||||
|
|
||||||
|
## Scheduled releases on the host
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
Four systemd units in `systemd/` drive unattended releases from the host.
|
||||||
|
|
||||||
|
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 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
|
||||||
|
|
||||||
|
That cadence is only safe because of three guards:
|
||||||
|
|
||||||
|
- **No overlap.** Every channel-mutating run takes a host-wide lock
|
||||||
|
(`pkgs.omarchy.org/.release.lock`). Scheduled runs take it
|
||||||
|
**non-blocking**: if a build is already going, the tick exits immediately
|
||||||
|
instead of queuing. Waiting would stack one stalled process per tick behind
|
||||||
|
a long build and stampede when it finished. Manual commands still wait, as
|
||||||
|
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>-<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>-<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.
|
||||||
|
|
||||||
|
`bin/repo timers` reports all of this: schedules, last results, what is
|
||||||
|
queued, what is failing and when it will retry, and whether the lock is held.
|
||||||
|
|
||||||
|
## Build reports
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
Every release run reports to Basecamp Campfire — not just failures, so a push
|
||||||
|
can be followed all the way to the mirror without watching the host:
|
||||||
|
|
||||||
|
| Event | Message |
|
||||||
|
|---|---|
|
||||||
|
| 🔨 Started | channel, arch, host, the commit being built, and which packages are queued |
|
||||||
|
| ✅ Published | the packages and versions that went out, duration, and the channel URL |
|
||||||
|
| 📦 Promoted | what `advance` moved between channels (including the rc bootstrap) |
|
||||||
|
| 📦 Nothing to publish | a queued run built nothing — see below |
|
||||||
|
| 🔴 Failed | which step failed, the commit, and the last 25 log lines |
|
||||||
|
|
||||||
|
Reports are tied to release *runs*, not timer ticks: a tick with nothing
|
||||||
|
queued exits silently, so a quiet day is a quiet chat. That is what makes the
|
||||||
|
"nothing to publish" report meaningful rather than routine — a release only
|
||||||
|
runs when the version check queued work, so building nothing means the check
|
||||||
|
and the builder disagree about what is out of date. It names the packages that
|
||||||
|
were queued but not built, which is where to start looking.
|
||||||
|
|
||||||
|
By default everything posts to `BASECAMP_CHATBOT_URL`, the same chat the
|
||||||
|
sync workflows use — one variable, nothing extra to configure.
|
||||||
|
|
||||||
|
If release traffic starts drowning that chat, give it its own: create a second
|
||||||
|
Basecamp chat, add a chatbot integration to it, and export its lines URL
|
||||||
|
alongside the existing one in `/root/.omarchy/build-credentials`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export OMARCHY_RELEASE_CHATBOT_URL="https://3.basecamp.com/<account>/integrations/<key>/buckets/<project>/chats/<chat>/lines"
|
||||||
|
```
|
||||||
|
|
||||||
|
Release reports then go there while the sync workflows keep posting to
|
||||||
|
`BASECAMP_CHATBOT_URL`. With neither set, reports are silently skipped.
|
||||||
|
`bin/setup` reports which of the three cases applies.
|
||||||
|
|
||||||
|
Check on all of it with `bin/repo timers` — schedule, each unit's last run and
|
||||||
|
whether it succeeded, what is queued, whether a release is running right now,
|
||||||
|
and any failed units. Like the other host commands it forwards over ssh, so
|
||||||
|
the build box's state is one command away from any machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo timers # runs on the repository host when one is configured
|
||||||
|
bin/repo timers --local # inspect this machine instead
|
||||||
|
```
|
||||||
|
|
||||||
|
State files are stored in `/root/.state/`:
|
||||||
|
- `.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)
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
| Minute of every hour | Action |
|
||||||
|
|------|--------|
|
||||||
|
| :00, :05, :10, … | check-versions (git pull + creates state files) |
|
||||||
|
| :01, :06, :11, … | auto-release-edge |
|
||||||
|
| :02, :07, :12, … | auto-release-rc |
|
||||||
|
| :03, :08, :13, … | auto-release-stable |
|
||||||
|
|
||||||
|
Each release unit is a no-op when its channel has nothing queued, another run
|
||||||
|
holds the lock, or the channel is in failure backoff.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@<host> 'cd /root/omarchy-pkgs && bin/setup --skip-timers'
|
||||||
|
```
|
||||||
|
|
||||||
|
`bin/setup` installs the dependencies, ensures Docker is running and creates
|
||||||
|
the state directory. Without `--skip-timers` it also installs and enables the
|
||||||
|
release timers, which must stay off while CI publishes. It works on
|
||||||
|
Debian/Ubuntu and on Arch, and is idempotent, so run it again whenever a
|
||||||
|
dependency is added.
|
||||||
|
|
||||||
|
The host does not need to be Arch: makepkg, repo-add and package signing all
|
||||||
|
run inside containers, so it needs only Docker, rclone, bsdtar, jq, git and
|
||||||
|
rsync. Docker is left alone when it already works, rather than replacing a
|
||||||
|
working installation from Docker's own repository with the distribution's.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/repo setup --check # Report what is missing, change nothing
|
||||||
|
bin/repo setup --skip-timers # Prepare the host without the release timers
|
||||||
|
```
|
||||||
|
|
||||||
|
Signing credentials (`/root/.omarchy/build-credentials`) and the rclone remote
|
||||||
|
hold secrets, so setup reports on them rather than creating them.
|
||||||
|
|
||||||
|
## Management
|
||||||
|
|
||||||
|
> Abandoned. Kept as reference for the tooling; don't follow it to publish.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Everything at a glance: schedules, last runs, queues, backoff, lock
|
||||||
|
bin/repo timers # forwards to the host when one is configured
|
||||||
|
|
||||||
|
# Manual trigger (edge, rc, stable)
|
||||||
|
systemctl start omarchy-check-versions.service
|
||||||
|
systemctl start omarchy-auto-release-edge.service
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
journalctl -u omarchy-auto-release-edge.service -n 50
|
||||||
|
|
||||||
|
# Clear a channel stuck in failure backoff (a new commit also clears it)
|
||||||
|
rm /root/.state/.build-failed-edge-x86_64
|
||||||
|
|
||||||
|
# Release lock left behind by a killed run (bin/repo timers shows if it is live)
|
||||||
|
rm /root/omarchy-pkgs/pkgs.omarchy.org/.release.lock
|
||||||
|
```
|
||||||
|
|
||||||
|
With the timers enabled, `check-versions` pulls the main checkout on its
|
||||||
|
5-minute tick. With them disabled, nothing does: `bin/repo` pulls before a
|
||||||
|
command it forwards, and a command run on the host itself needs a `git pull`
|
||||||
|
first. The checkout must be on `master`.
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
# 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 falls back to the newest commit on the branch's first-parent
|
||||||
|
history that has cleared the window, so a busy branch still advances; it never
|
||||||
|
selects a commit from a merged side branch. This uses Git's committer date, not
|
||||||
|
the time a commit was pushed, measured from when the run cloned the branch so
|
||||||
|
packages pinned from it agree. `BYPASS_MIN_RELEASE_AGE=1` bypasses the hold.
|
||||||
|
|
||||||
|
Packages marked `"auto_merge": true` ride the unattended lane
|
||||||
|
(`track-branches.yml`), checked hourly instead of every 6 hours. Every lane
|
||||||
|
opens its PRs through `package-prs.yml` and auto-merges them as soon as the
|
||||||
|
build checks pass. Each package gets its own
|
||||||
|
PR on `auto/track-branches-<package>`, and packages that pin the same branch
|
||||||
|
share one, so a package whose build fails holds back only itself. The lane works with every
|
||||||
|
provider, not only branch watches, so it also carries release feeds trusted to
|
||||||
|
ship without review: the browsers, large vendors' apps, Omacom's own projects,
|
||||||
|
and vendor binaries that hold fresh releases with `min_release_age`. `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.
|
||||||
|
|
||||||
|
## How the sync PRs build
|
||||||
|
|
||||||
|
`sync-upstream.yml`, `sync-rebuilds.yml` and `track-branches.yml` each run their
|
||||||
|
sync once, split the changed recipes into one PR per package with
|
||||||
|
`.github/scripts/package-pr-groups.sh`, and hand them to `package-prs.yml`. That
|
||||||
|
pushes each PR with the PAT in `PKGS_BOT_TOKEN`, so its builds start without an
|
||||||
|
approval hold, and arms auto-merge. A push to an `auto/sync-*` or
|
||||||
|
`auto/track-branches-*` branch does not cancel the PR's build in flight: the new
|
||||||
|
build waits, then reuses every artifact the finished one uploaded.
|
||||||
|
|
||||||
|
A red PR stays open for a maintainer and holds back only its own package. A
|
||||||
|
later run updates it if upstream moves again, and closes it once it conflicts
|
||||||
|
with `master`. A run started by hand with `packages` updates the same
|
||||||
|
per-package PRs a scheduled run would.
|
||||||
|
|
||||||
|
## Declarative providers and hooks
|
||||||
|
|
||||||
|
The older provider form, still in use beside `upstream.watch`. Give a new
|
||||||
|
package an `upstream.watch`; `helpers/upstream-watch.py check` validates that
|
||||||
|
form only.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/sync-upstream # Update every package with an upstream hook
|
||||||
|
bin/sync-upstream openai-codex-desktop # Update specific packages
|
||||||
|
```
|
||||||
|
|
||||||
|
A package declares where its releases come from either as data or, for an
|
||||||
|
unusual feed, a small hook.
|
||||||
|
|
||||||
|
A vendor shipping tagged GitHub releases is pure data, declared as `upstream`
|
||||||
|
in `.omarchy/package.json` with no code at all:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"upstream": {
|
||||||
|
"github": "jdx/mise",
|
||||||
|
"checksums": "SHASUMS256.txt",
|
||||||
|
"assets": {
|
||||||
|
"x86_64": "mise-{tag}-linux-x64.tar.xz",
|
||||||
|
"aarch64": "mise-{tag}-linux-arm64.tar.xz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
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
|
||||||
|
result through a normal PR; scheduled automation never sets the bypass.
|
||||||
|
|
||||||
|
A vendor whose feed fits no convention (a Debian package index, a bare
|
||||||
|
version.txt) instead provides `.omarchy/upstream.sh`, a hook that reports the
|
||||||
|
newest upstream release as JSON on stdout — declaring both an `upstream` block
|
||||||
|
and a hook is an error:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pkgver": "1.2.3",
|
||||||
|
"sha256sums": { "x86_64": ["<sha256>"], "aarch64": ["<sha256>"] }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Architecture keys become `sha256sums_<arch>` in the PKGBUILD; the key `any` means
|
||||||
|
the unsuffixed `sha256sums` array, and only the arrays a hook names are touched.
|
||||||
|
An empty object (`{}`) reports no update, which is how a hook waits out a release
|
||||||
|
that has landed for one architecture but not yet the other.
|
||||||
|
|
||||||
|
When the reported version is newer than the checked-in one, `bin/sync-upstream`
|
||||||
|
rewrites `pkgver` and those checksum arrays and resets `pkgrel` to 1. A version
|
||||||
|
that is equal or older leaves the package alone, so a vendor rolling a release
|
||||||
|
back cannot walk the repository backwards.
|
||||||
|
|
||||||
|
Hooks should read checksums from whatever manifest the vendor publishes rather
|
||||||
|
than downloading the artifacts — see `pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh`,
|
||||||
|
which reads OpenAI's Debian package index and never fetches the 750 MB of debs
|
||||||
|
it describes. Hooks honoring `min_release_age` receive the window as
|
||||||
|
`MIN_RELEASE_AGE_SECONDS` and report `published_at` alongside `pkgver`.
|
||||||
|
|
||||||
|
`bin/sync-upstream self-test` runs offline fixture tests over the release
|
||||||
|
selection, quarantine backstop, duration parsing, and manifest validation.
|
||||||
@@ -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() {
|
container_engine_supported() {
|
||||||
if ! command -v docker &>/dev/null; then
|
[[ "$CONTAINER_ENGINE" == "docker" || "$CONTAINER_ENGINE" == "podman" ]]
|
||||||
print_error "Docker is not installed"
|
}
|
||||||
|
|
||||||
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! docker info &>/dev/null; then
|
if [[ -z "$CONTAINER_ENGINE" ]]; then
|
||||||
print_error "Docker daemon is not running"
|
print_error "No working container engine found (tried Docker, then Podman)"
|
||||||
print_warning "Start Docker with: sudo systemctl start docker"
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
docker_native_arch() {
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64) echo x86_64 ;;
|
||||||
|
aarch64 | arm64) echo aarch64 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
setup_qemu() {
|
setup_qemu() {
|
||||||
# Setup QEMU for building ARM64 packages on x86_64 hosts
|
local target_arch="${1:-aarch64}"
|
||||||
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"
|
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
|
exit 1
|
||||||
fi
|
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() {
|
build_docker_image() {
|
||||||
@@ -39,31 +127,45 @@ build_docker_image() {
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
print_info "Building Docker image for $arch ($platform) using $mirror mirror..."
|
print_info "Building container image for $arch ($platform) using $mirror mirror..."
|
||||||
|
|
||||||
docker buildx build \
|
if [[ "$CONTAINER_ENGINE" == "docker" ]]; then
|
||||||
--platform "$platform" \
|
"$CONTAINER_ENGINE" buildx build \
|
||||||
--build-arg MIRROR="$mirror" \
|
--platform "$platform" \
|
||||||
--load \
|
--build-arg MIRROR="$mirror" \
|
||||||
-t "$image_tag" \
|
--load \
|
||||||
-f "$build_dir/Dockerfile" \
|
-t "$image_tag" \
|
||||||
"$build_dir"
|
-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() {
|
get_platform_arg() {
|
||||||
local arch="$1"
|
local arch="$1"
|
||||||
case "$arch" in
|
case "$arch" in
|
||||||
x86_64) echo "--platform linux/amd64" ;;
|
x86_64) echo "--platform=linux/amd64" ;;
|
||||||
aarch64) echo "--platform linux/arm64" ;;
|
aarch64) echo "--platform=linux/arm64" ;;
|
||||||
*) echo "" ;;
|
*) return 1 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
make_dir_writable() {
|
make_dir_writable() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
if [ "$(id -u)" -eq 0 ]; then
|
if (( EUID == 0 )); then
|
||||||
chmod -R 777 "$dir"
|
chmod -R 777 "$dir"
|
||||||
else
|
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
|
fi
|
||||||
}
|
}
|
||||||
+181
-20
@@ -3,21 +3,24 @@
|
|||||||
# Expects package directories in $PKGBUILDS_DIR, each with:
|
# Expects package directories in $PKGBUILDS_DIR, each with:
|
||||||
# .omarchy/package.json
|
# .omarchy/package.json
|
||||||
#
|
#
|
||||||
# Minimal schema:
|
# Minimal schema (legacy source:aur remains readable for initial imports):
|
||||||
# { "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"] }
|
|
||||||
# { "source": "local" }
|
# { "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"] }
|
||||||
# { "source": "local", "channels": ["edge", "rc", "stable"] }
|
# { "source": "local", "channels": ["edge", "rc", "stable"] }
|
||||||
# { "source": "local", "min_release_age": "24h" }
|
# { "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.
|
# bin/sync-rebuilds writes rebuilt_against for packages declaring rebuild_on.
|
||||||
|
|
||||||
if [[ -z "${PKGBUILDS_DIR:-}" ]]; then
|
if [[ -z "${PKGBUILDS_DIR:-}" ]]; then
|
||||||
@@ -140,6 +143,67 @@ package_has_pkgbuild() {
|
|||||||
[[ -f "$pkgdir/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
|
# Channel membership: where a package may be published. Packages without a
|
||||||
# `channels` key are members of every channel (they flow edge -> rc -> stable).
|
# `channels` key are members of every channel (they flow edge -> rc -> stable).
|
||||||
package_has_channels() {
|
package_has_channels() {
|
||||||
@@ -216,6 +280,11 @@ package_builds_for_mirror() {
|
|||||||
package_moves_to_channel() {
|
package_moves_to_channel() {
|
||||||
local pkgdir="$1" channel="$2"
|
local pkgdir="$1" channel="$2"
|
||||||
package_in_channel "$pkgdir" "$channel" || return 1
|
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"
|
! package_builds_for_mirror "$pkgdir" "$channel"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,6 +328,32 @@ packages_for_upstream_sync() {
|
|||||||
done
|
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, and how trusted release feeds (browsers, large
|
||||||
|
# vendors, Omacom's own projects) ship without waiting for review. 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,
|
# Packages that must be rebuilt when a dependency they link against changes,
|
||||||
# even though nothing in their own source moved. `rebuild_on` names those
|
# even though nothing in their own source moved. `rebuild_on` names those
|
||||||
# dependencies; `rebuilt_against` records the versions the checked-in pkgrel was
|
# dependencies; `rebuilt_against` records the versions the checked-in pkgrel was
|
||||||
@@ -298,9 +393,12 @@ packages_for_mirror() {
|
|||||||
|
|
||||||
packages_for_unscoped_build() {
|
packages_for_unscoped_build() {
|
||||||
local mirror="$1"
|
local mirror="$1"
|
||||||
|
local arch="${2:-${ARCH:-x86_64}}"
|
||||||
|
|
||||||
package_dirs | while IFS= read -r pkgdir; do
|
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"
|
basename "$pkgdir"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -443,23 +541,70 @@ validate_package_metadata() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
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
|
# `has` rather than `// {}`: jq's // treats false as absent, which would
|
||||||
# let "upstream": false slip through as an empty declaration.
|
# let "upstream": false slip through as an empty declaration.
|
||||||
if ! jq -e '
|
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
|
if has("upstream") | not then true
|
||||||
elif (.upstream | type) != "object" then false
|
elif (.upstream | type) != "object" then false
|
||||||
else .upstream |
|
else .upstream |
|
||||||
((.github // "") | type == "string" and test("\\A[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\\z"))
|
([has("github"), has("git_tags"), has("npm"), has("debian"), has("watch")] | map(select(.)) | length) == 1
|
||||||
and ((.checksums // "") | type == "string" and length > 0)
|
and if has("watch") then (.watch | type == "object")
|
||||||
and ((.assets // {}) | type == "object" and length > 0 and (to_entries | all(
|
elif has("github") then
|
||||||
(.key | test("\\A[a-z0-9_]+\\z")) and (.value | type == "string" and length > 0)
|
(.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
|
end
|
||||||
' "$metadata" >/dev/null; then
|
' "$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
|
return 1
|
||||||
fi
|
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")
|
pkgrel_type=$(jq -r 'if has("pkgrel") then .pkgrel | type else "missing" end' "$metadata")
|
||||||
case "$pkgrel_type" in
|
case "$pkgrel_type" in
|
||||||
object|missing) ;;
|
object|missing) ;;
|
||||||
@@ -481,12 +626,28 @@ validate_package_metadata() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! jq -e '(.rebuilt_against // {}) | type == "object" and (to_entries | all(.value | type == "string" and length > 0))' "$metadata" >/dev/null; then
|
if ! jq -e '
|
||||||
echo "invalid rebuilt_against for $(basename "$pkgdir"): must be an object mapping package names to versions"
|
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
|
return 1
|
||||||
fi
|
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"
|
echo "invalid rebuilt_against for $(basename "$pkgdir"): records a package that rebuild_on does not name"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,6 +5,76 @@
|
|||||||
ARCH=${ARCH:-x86_64}
|
ARCH=${ARCH:-x86_64}
|
||||||
MIRROR=${MIRROR:-edge}
|
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 package channels, in pipeline order: packages move edge -> rc -> stable
|
||||||
VALID_MIRRORS="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
|
# A package whose upstream ships tagged GitHub releases needs no upstream.sh
|
||||||
# manifest asset needs no upstream.sh hook: the whole feed is data, declared
|
# hook: the whole feed is data, declared in .omarchy/package.json --
|
||||||
# in .omarchy/package.json --
|
|
||||||
#
|
#
|
||||||
# "upstream": {
|
# "upstream": {
|
||||||
# "github": "jdx/mise",
|
# "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
|
# {tag} and {pkgver} interpolate into asset names; tags may carry a leading
|
||||||
# "v", which is stripped for pkgver. Drafts and prereleases are ignored. The
|
# "v", which is stripped for pkgver. Drafts and prereleases are ignored. The
|
||||||
# provider emits the same JSON contract as an upstream.sh hook, so
|
# provider emits the same JSON contract as an upstream.sh hook, so
|
||||||
# bin/sync-upstream's validation and min_release_age backstop apply
|
# 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() {
|
# Return the single declarative provider selected by a package. An empty
|
||||||
local pkgdir="$1"
|
# result means either no provider or an invalid/ambiguous declaration; the
|
||||||
# `objects` drops a non-object upstream value (validation rejects those
|
# caller distinguishes those through package_has_upstream_provider().
|
||||||
# separately) instead of erroring the jq pipeline.
|
package_upstream_provider() {
|
||||||
package_metadata_value "$pkgdir" '(.upstream? | objects | .github)' ""
|
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
|
# 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"
|
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
|
# Emits the newest qualifying release as hook-contract JSON. min_release_age
|
||||||
# is honored during selection (newest release older than the window wins,
|
# 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.
|
# 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.
|
# not silently choose from.
|
||||||
github_upstream_release() {
|
github_upstream_release() {
|
||||||
local package_dir="$1" min_age="${2:-0}"
|
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")
|
metadata=$(metadata_file_for_dir "$package_dir")
|
||||||
|
|
||||||
repo=$(jq -r '(.upstream? | objects | .github) // ""' "$metadata")
|
repo=$(jq -r '(.upstream? | objects | .github) // ""' "$metadata")
|
||||||
@@ -59,9 +284,46 @@ github_upstream_release() {
|
|||||||
echo "invalid upstream.github repository: '${repo:-<empty>}'" >&2
|
echo "invalid upstream.github repository: '${repo:-<empty>}'" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
checksums_name=$(jq -r '(.upstream? | objects | .checksums) // ""' "$metadata")
|
# Enforced here as well as in validate_package_metadata: the scheduled sync
|
||||||
if [[ -z "$checksums_name" ]]; then
|
# reaches this provider without running the validator first.
|
||||||
echo "upstream.checksums names the checksum manifest asset and is required" >&2
|
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
|
return 1
|
||||||
fi
|
fi
|
||||||
local arches
|
local arches
|
||||||
@@ -77,6 +339,9 @@ github_upstream_release() {
|
|||||||
echo "could not fetch the release feed for $repo" >&2
|
echo "could not fetch the release feed for $repo" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
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 candidates=0 best_tag="" best_pkgver="" best_published_at=""
|
||||||
local tag published_at pkgver published_epoch
|
local tag published_at pkgver published_epoch
|
||||||
@@ -129,33 +394,57 @@ github_upstream_release() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local checksums
|
local checksums=""
|
||||||
if ! checksums=$(github_fetch_checksums "$repo" "$best_tag" "$checksums_name"); then
|
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
|
echo "could not fetch $checksums_name for $repo $best_tag" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local jq_args=(--arg pkgver "$best_pkgver" --arg published_at "$best_published_at")
|
local result
|
||||||
local jq_filter='{pkgver: $pkgver, published_at: $published_at, sha256sums: {}}'
|
result=$(jq -n --arg pkgver "$best_pkgver" --arg published_at "$best_published_at" \
|
||||||
local arch template filename checksum
|
'{pkgver: $pkgver, published_at: $published_at, sha256sums: {}}')
|
||||||
|
local arch template filename checksum checksum_source sums
|
||||||
for arch in "${arches[@]}"; do
|
for arch in "${arches[@]}"; do
|
||||||
if [[ ! "$arch" =~ ^[a-z0-9_]+$ ]]; then
|
if [[ ! "$arch" =~ ^[a-z0-9_]+$ ]]; then
|
||||||
echo "invalid architecture key in upstream.assets: '$arch'" >&2
|
echo "invalid architecture key in upstream.assets: '$arch'" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
template=$(jq -r --arg arch "$arch" '.upstream.assets[$arch]' "$metadata")
|
sums='[]'
|
||||||
filename=${template//\{pkgver\}/$best_pkgver}
|
while IFS= read -r template; do
|
||||||
filename=${filename//\{tag\}/$best_tag}
|
filename=${template//\{pkgver\}/$best_pkgver}
|
||||||
# Manifest lines are "<sha256> <name>", with the name sometimes prefixed
|
filename=${filename//\{tag\}/$best_tag}
|
||||||
# "./" (sha256sum of a local path) or "*" (binary-mode marker).
|
if [[ "$use_digests" == "true" ]]; then
|
||||||
checksum=$(awk -v f="$filename" '$2 == f || $2 == "./" f || $2 == "*" f { print $1; exit }' <<<"$checksums")
|
# Only a "sha256:<hex>" digest is stripped to its hex; any other shape
|
||||||
if [[ ! "$checksum" =~ ^[0-9a-f]{64}$ ]]; then
|
# falls through empty and fails the check below.
|
||||||
echo "no valid checksum for $filename in $repo $best_tag $checksums_name" >&2
|
checksum=$(jq -r --arg tag "$best_tag" --arg name "$filename" '
|
||||||
return 1
|
first(.[] | select(.tag_name == $tag)) | (.assets // [])[]
|
||||||
fi
|
| select(.name == $name) | (.digest // "")
|
||||||
jq_args+=(--arg "sum_$arch" "$checksum")
|
| if type == "string" and test("\\Asha256:[0-9a-f]{64}\\z") then ltrimstr("sha256:") else "" end
|
||||||
jq_filter+=" | .sha256sums[\"$arch\"] = [\$sum_$arch]"
|
' <<<"$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
|
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,644 @@
|
|||||||
|
#!/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, min_age=0):
|
||||||
|
"""Describe the newest commit on an upstream branch that has cleared
|
||||||
|
min_age: 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.
|
||||||
|
A tip younger than min_age falls back along first-parent history, the
|
||||||
|
commits that were themselves the branch tip, never into a merged side
|
||||||
|
branch. Holding the whole branch instead stalled a busy branch for as
|
||||||
|
long as it kept getting commits. Age is measured from when the clone was
|
||||||
|
made, so siblings synced seconds apart cannot straddle the window.
|
||||||
|
"""
|
||||||
|
https(url)
|
||||||
|
key = hashlib.sha256(f"{url}#{branch}".encode()).hexdigest()
|
||||||
|
work = Path(cache) / f"{key}.branch.git"
|
||||||
|
cloned_at = work.with_name(f"{key}.branch.time")
|
||||||
|
if not work.exists():
|
||||||
|
scratch = work.with_name(f"{work.name}.{os.getpid()}.tmp")
|
||||||
|
started = dt.datetime.now(dt.timezone.utc).isoformat()
|
||||||
|
subprocess.run(["git", "clone", "--quiet", "--bare", "--filter=blob:none", "--single-branch", "--branch", branch, url, str(scratch)], check=True)
|
||||||
|
cloned_at.write_text(started)
|
||||||
|
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")
|
||||||
|
if min_age:
|
||||||
|
now = dt.datetime.fromisoformat(cloned_at.read_text().strip())
|
||||||
|
for line in run([*git, "log", "--first-parent", "--format=%H %cI", commit], text=True).splitlines():
|
||||||
|
sha, committed = line.split()
|
||||||
|
if (now - dt.datetime.fromisoformat(committed)).total_seconds() >= min_age:
|
||||||
|
commit = sha
|
||||||
|
break
|
||||||
|
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, min_age=0):
|
||||||
|
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, min_age)
|
||||||
|
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, 0 if bypass else min_age), 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",
|
"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
|
pkgname=1password-beta
|
||||||
|
|
||||||
_tarver=8.12.34-29.BETA
|
_tarver=8.12.42-32.BETA
|
||||||
case "${CARCH}" in
|
case "${CARCH}" in
|
||||||
x86_64)
|
x86_64)
|
||||||
_archdir="x64"
|
_archdir="x64"
|
||||||
@@ -9,8 +9,8 @@ case "${CARCH}" in
|
|||||||
_archdir="arm64"
|
_archdir="arm64"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
pkgver=${_tarver//-/_}
|
pkgver=8.12.42_32.BETA
|
||||||
pkgrel=29.1
|
pkgrel=1
|
||||||
conflicts=('1password' '1password-beta-bin')
|
conflicts=('1password' '1password-beta-bin')
|
||||||
pkgdesc="Password manager and secure wallet"
|
pkgdesc="Password manager and secure wallet"
|
||||||
arch=('x86_64' 'aarch64')
|
arch=('x86_64' 'aarch64')
|
||||||
@@ -22,10 +22,8 @@ source=()
|
|||||||
sha256sums=()
|
sha256sums=()
|
||||||
source_x86_64=(https://downloads.1password.com/linux/tar/beta/x86_64/1password-${_tarver}.x64.tar.gz{,.sig})
|
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})
|
source_aarch64=(https://downloads.1password.com/linux/tar/beta/aarch64/1password-${_tarver}.arm64.tar.gz{,.sig})
|
||||||
sha256sums_x86_64=('6894b283a534cf94b07903fb38966a0aab2e37f75ee3848ce340308819aadddb'
|
sha256sums_x86_64=('55fe8dbc80e65337e657c926948dff356b09c3d0860f3b381f1df5af2b99741a' '62494cae0f90d553cade1eeda82cec4d5b26e4719f2233085ce47a70cc80da63')
|
||||||
'8d4df4d0a80d2be7aad7d91ad964a750c8f32db5007261045003c191690c8246')
|
sha256sums_aarch64=('53a63a857e9219da410839915216c4f9cc017cb17bbff9095caf01365e28c5d8' 'd910aedf72c4ff31745b0bbfc12d03c03b177df85f73a381a35227439320ad5c')
|
||||||
sha256sums_aarch64=('c77ce6ddf36dbd6054c64d91b7f644274d3218f465fd60e211d0296f6443124a'
|
|
||||||
'91c7249a1cf5e7924ef2810cb0cb1b893d8a9a424b373b433f45d7aaa5a8369b')
|
|
||||||
validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22')
|
validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22')
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
@@ -42,7 +40,7 @@ package() {
|
|||||||
"${pkgdir}/usr/share/icons/hicolor/${resolution}/apps/1password.png"
|
"${pkgdir}/usr/share/icons/hicolor/${resolution}/apps/1password.png"
|
||||||
done
|
done
|
||||||
# Install desktop file
|
# 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.
|
# Fill in policy kit file with a list of (the first 10) human users of the system.
|
||||||
export POLICY_OWNERS
|
export POLICY_OWNERS
|
||||||
@@ -65,8 +63,7 @@ EOF" > ./com.1password.1Password.policy
|
|||||||
# Cleanup un-needed files
|
# 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 "${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 -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
|
# Symlink /usr/bin executable to opt
|
||||||
install -dm0755 "${pkgdir}"/usr/bin
|
install -dm0755 "${pkgdir}"/usr/bin
|
||||||
ln -s /opt/1Password/1password "${pkgdir}"/usr/bin/1password
|
ln -s /opt/1Password/1password "${pkgdir}"/usr/bin/1password
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
{
|
{
|
||||||
"source": "aur",
|
"source": "local",
|
||||||
|
"auto_merge": true,
|
||||||
"release_ring": "fast",
|
"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
|
# Contributors: Felix Seidel, Claudia Pellegrino, Liu Yuxuan
|
||||||
|
|
||||||
pkgname=1password-cli
|
pkgname=1password-cli
|
||||||
pkgver=2.39.0
|
pkgver=2.40.0
|
||||||
pkgrel=1
|
pkgrel=2
|
||||||
pkgdesc="1Password command line tool"
|
pkgdesc="1Password command line tool"
|
||||||
arch=('x86_64' 'i686' 'arm' 'armv6h' 'aarch64')
|
arch=('x86_64' 'i686' 'arm' 'armv6h' 'aarch64')
|
||||||
url="https://app-updates.agilebits.com/product_history/CLI2"
|
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_armv6h=("${source_arm}")
|
||||||
source_aarch64=("https://cache.agilebits.com/dist/1P/op2/pkg/v${pkgver}/op_linux_arm64_v${pkgver}.zip")
|
source_aarch64=("https://cache.agilebits.com/dist/1P/op2/pkg/v${pkgver}/op_linux_arm64_v${pkgver}.zip")
|
||||||
|
|
||||||
sha256sums_x86_64=('6fba7f376b6c6dec49f41b06408930a43ad064cce103c6a2ce5b3d0413a86434')
|
sha256sums_x86_64=('74277219e8da60958c00f9aee9d2023225e98fdda8bfd2156a5d9e85e0edaab3')
|
||||||
sha256sums_i686=('387d95f046ec9334e9de63514f96767fdd5dacbae292eb086fdfa0b0da310ec4')
|
sha256sums_i686=('adb1da45c0a40bb97c1e1ad62119d0a1aeb4ed9d979c06a7b1f95a48d9142b3d')
|
||||||
sha256sums_arm=('673913f24ff57ce43e9d25ee451121d4733d188f45f6ab0468983fad85f8cc42')
|
sha256sums_arm=('cb9a2e938b542eac668e847bf555293549ad6ecc014216c99a66f1348728e354')
|
||||||
sha256sums_armv6h=("${sha256sums_arm}")
|
sha256sums_armv6h=('cb9a2e938b542eac668e847bf555293549ad6ecc014216c99a66f1348728e354')
|
||||||
sha256sums_aarch64=('829baeff1c07e055cfa132031b1d9f2282ccdf5076258e482caf2fda70aea5d0')
|
sha256sums_aarch64=('0e8ac99ee93d661aa725dc24a5ef8bf344741d224064a5dfb469dc689faec86a')
|
||||||
|
|
||||||
check() {
|
check() {
|
||||||
if (( ! SKIPPGPCHECK )); then
|
if (( ! SKIPPGPCHECK )); then
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
{
|
{
|
||||||
"source": "aur",
|
"source": "local",
|
||||||
|
"auto_merge": true,
|
||||||
"release_ring": "fast",
|
"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
|
pkgname=1password
|
||||||
|
pkgver=8.12.40
|
||||||
_tarver=8.12.34
|
pkgrel=2
|
||||||
_tar="1password-${_tarver}.x64.tar.gz"
|
|
||||||
pkgver=${_tarver//-/_}
|
|
||||||
pkgrel=34
|
|
||||||
conflicts=('1password-beta' '1password-beta-bin')
|
conflicts=('1password-beta' '1password-beta-bin')
|
||||||
pkgdesc="Password manager and secure wallet"
|
pkgdesc="Password manager and secure wallet"
|
||||||
arch=('x86_64')
|
arch=('x86_64' 'aarch64')
|
||||||
url='https://1password.com'
|
url='https://1password.com'
|
||||||
license=('LicenseRef-1Password-Proprietary')
|
license=('LicenseRef-1Password-Proprietary')
|
||||||
options=(!strip)
|
options=(!strip)
|
||||||
install="1password.install"
|
install="1password.install"
|
||||||
source=(https://downloads.1password.com/linux/tar/stable/${CARCH}/${_tar}{,.sig})
|
source_x86_64=(
|
||||||
sha256sums=('297784aa66770b645607a7f04c9ba2c4aebed4f46d21202487f521ba572b7b13'
|
"https://downloads.1password.com/linux/tar/stable/x86_64/1password-${pkgver}.x64.tar.gz"
|
||||||
'ec085bef60de748895d3c51a8208301ba2ac8fb47db99334539ba4bd1d3260d7'
|
"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')
|
validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22')
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
depends=('hicolor-icon-theme' 'libgtk-3.so=0' 'nss' 'xdg-utils')
|
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
|
# Go to source directory
|
||||||
cd "1password-${_tarver}.x64"
|
cd "1password-${pkgver}.${_tararch}"
|
||||||
|
|
||||||
# Install icons
|
# Install icons
|
||||||
resolutions=(32x32 64x64 256x256 512x512)
|
resolutions=(32x32 64x64 256x256 512x512)
|
||||||
@@ -31,7 +51,14 @@ package() {
|
|||||||
"${pkgdir}/usr/share/icons/hicolor/${resolution}/apps/1password.png"
|
"${pkgdir}/usr/share/icons/hicolor/${resolution}/apps/1password.png"
|
||||||
done
|
done
|
||||||
# Install desktop file
|
# 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.
|
# Fill in policy kit file with a list of (the first 10) human users of the system.
|
||||||
export POLICY_OWNERS
|
export POLICY_OWNERS
|
||||||
@@ -49,12 +76,12 @@ EOF" > ./com.1password.1Password.policy
|
|||||||
# Move package contents to /opt/1Password
|
# Move package contents to /opt/1Password
|
||||||
cd "${srcdir}"
|
cd "${srcdir}"
|
||||||
install -dm0755 "${pkgdir}"/opt
|
install -dm0755 "${pkgdir}"/opt
|
||||||
mv "1password-${_tarver}.x64" "${pkgdir}/opt/1Password"
|
mv "1password-${pkgver}.${_tararch}" "${pkgdir}/opt/1Password"
|
||||||
|
|
||||||
# Cleanup un-needed files
|
# 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 "${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 -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
|
# Symlink /usr/bin executable to opt
|
||||||
install -dm0755 "${pkgdir}"/usr/bin
|
install -dm0755 "${pkgdir}"/usr/bin
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
{
|
{
|
||||||
"source": "aur",
|
"source": "local",
|
||||||
|
"auto_merge": true,
|
||||||
"release_ring": "fast",
|
"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>
|
# Maintainer: Bjarne Øverli <bjarne@oever.li>
|
||||||
pkgname=aether
|
pkgname=aether
|
||||||
pkgver=4.29.6
|
pkgver=4.32.0
|
||||||
pkgrel=1
|
pkgrel=2
|
||||||
pkgdesc='Desktop theming application - extract colors from wallpapers and apply cohesive themes'
|
pkgdesc='Desktop theming application - extract colors from wallpapers and apply cohesive themes'
|
||||||
arch=('x86_64' 'aarch64')
|
arch=('x86_64' 'aarch64')
|
||||||
url='https://github.com/omacom-io/aether'
|
url='https://github.com/omacom/aether'
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=('webkit2gtk-4.1' 'gtk3')
|
depends=('webkit2gtk-4.1' 'gtk3')
|
||||||
source=("aether-${pkgver}.tar.gz::https://github.com/omacom-io/aether/archive/refs/tags/v${pkgver}.tar.gz")
|
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-io/aether/releases/download/v${pkgver}/aether-linux-amd64")
|
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-io/aether/releases/download/v${pkgver}/aether-linux-arm64")
|
source_aarch64=("aether-linux-arm64-${pkgver}::https://github.com/omacom/aether/releases/download/v${pkgver}/aether-linux-arm64")
|
||||||
sha256sums=('8b2e97283007c9eefda879e17c5fecb9d59fc90bf2160af93eeba57f11fcdb25')
|
sha256sums=('3234e5138a46699f8f47330ba582244d5063c8356ffaae1458323e4dda02be82')
|
||||||
sha256sums_x86_64=('47b5afa4144b3a3cd7755524956ffe6b074ed5f2f90dadda23e424efeaf88790')
|
sha256sums_x86_64=('c705a4abef734b17ae37cdbff58a9a796517e6aa5f93ef18b68e3ee99d547ff9')
|
||||||
sha256sums_aarch64=('52d1ffb201970ddb6205882a8e1c583cae63470d4180f4bc2fc6298dfb71ddd9')
|
sha256sums_aarch64=('f70195deba008204d58661c3ec1442a16e63f3358b4579969facb0e55dbf63c4')
|
||||||
noextract=("aether-linux-amd64-${pkgver}" "aether-linux-arm64-${pkgver}")
|
noextract=("aether-linux-amd64-${pkgver}" "aether-linux-arm64-${pkgver}")
|
||||||
|
|
||||||
package() {
|
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
|
pkgver=0.6.0
|
||||||
pkgrel=2
|
pkgrel=2
|
||||||
pkgdesc="Control brightness on Apple Displays connected via USB-C"
|
pkgdesc="Control brightness on Apple Displays connected via USB-C"
|
||||||
arch=('x86_64')
|
arch=('x86_64' 'aarch64')
|
||||||
url="https://github.com/omakasui/asdcontrol"
|
url="https://github.com/omakasui/asdcontrol"
|
||||||
license=('GPL2')
|
license=('GPL2')
|
||||||
options=('!debug')
|
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,15 @@
|
|||||||
{
|
{
|
||||||
"source": "aur",
|
"source": "local",
|
||||||
|
"auto_merge": true,
|
||||||
"release_ring": "fast",
|
"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>
|
# Maintainer: Basecamp <support@basecamp.com>
|
||||||
pkgname=basecamp-cli
|
pkgname=basecamp-cli
|
||||||
pkgver=0.9.1
|
pkgver=0.13.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="CLI for Basecamp project management"
|
pkgdesc="CLI for Basecamp project management"
|
||||||
arch=('x86_64' 'aarch64')
|
arch=('x86_64' 'aarch64')
|
||||||
@@ -13,10 +13,10 @@ optdepends=(
|
|||||||
'zsh: for zsh shell completions'
|
'zsh: for zsh shell completions'
|
||||||
'fish: for fish 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_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/v0.9.1/basecamp_${pkgver}_linux_arm64.tar.gz")
|
source_aarch64=("https://github.com/basecamp/basecamp-cli/releases/download/v${pkgver}/basecamp_${pkgver}_linux_arm64.tar.gz")
|
||||||
sha256sums_x86_64=('bcb1fa3a03e702bbf81a3004ded4bc7808605e41106cbc768ba4006b89e0db2f')
|
sha256sums_x86_64=('8b33a93018c165f82ac7050d83cad8cde509f9cb7cfbccbc48fb9d98c0649931')
|
||||||
sha256sums_aarch64=('ff50313024b6ca14e40146e30e1c9c9c00a4e4103fe55615eeee3e7c359d88d1')
|
sha256sums_aarch64=('bef69d622d66685899b4f955b255951bb588cf1aac54f990b6bd818ae551104c')
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
install -Dm755 "basecamp" "${pkgdir}/usr/bin/basecamp"
|
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,16 @@
|
|||||||
|
{
|
||||||
|
"source": "local",
|
||||||
|
"auto_merge": true,
|
||||||
|
"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.97.56
|
||||||
|
pkgrel=1
|
||||||
|
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=('8acac76eae35942e30cc63e05adc772f632ad87be8a5ffb3d8a6e280380584ec')
|
||||||
|
sha256sums_aarch64=('f18823bfe8bf626a7412a2cc142ba851f991665ca31af8d750f80dbb8371f984')
|
||||||
|
|
||||||
|
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,16 @@
|
|||||||
|
{
|
||||||
|
"source": "local",
|
||||||
|
"auto_merge": true,
|
||||||
|
"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.97.56
|
||||||
|
pkgrel=1
|
||||||
|
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=('4710187525d4153d47c1d0366a69829f351b445f568ed091bf62215de25ddf34')
|
||||||
|
sha256sums_aarch64=('66789d7cd7d423f761d713edc9959ebff86b1493b1718c15346f627b02f6ef74')
|
||||||
|
|
||||||
|
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[@]}" "${@}"
|
||||||
Loaded 100 of 1590 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user