Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c9eb8b644 | ||
|
|
e2cc76091f | ||
|
|
4db3be34f8 | ||
|
|
b1e0c61df0 | ||
|
|
eb8482585b | ||
|
|
c207fea61e | ||
|
|
01c5864382 | ||
|
|
c01596aeb5 | ||
|
|
5d4de1d932 | ||
|
|
768b6dcdab | ||
|
|
253d72f7fb | ||
|
|
a1fa132c99 | ||
|
|
113f49f018 | ||
|
|
666d467b46 | ||
|
|
9c74c4d477 | ||
|
|
0b7efc3be5 | ||
|
|
4cfd003864 | ||
|
|
58adb1bef5 | ||
|
|
e4bd3420c6 | ||
|
|
e08f1ecba8 | ||
|
|
187da74794 | ||
|
|
c9935d1e0a | ||
|
|
cc7d5cf4ff | ||
|
|
8d9f1e6fde | ||
|
|
d61d91b6ae | ||
|
|
ba92bc4cb6 | ||
|
|
c353bfac3a | ||
|
|
9024a5a1fa | ||
|
|
35e4bba859 | ||
|
|
50535dbed0 | ||
|
|
f5ccb2a2a9 | ||
|
|
29ef97a86c | ||
|
|
8d646ce770 | ||
|
|
f63e51c96e | ||
|
|
4810e113f3 | ||
|
|
82c95d32c0 | ||
|
|
d4f92cd259 | ||
|
|
6b44835347 | ||
|
|
683196ddf5 | ||
|
|
1548ae8cd5 | ||
|
|
fd8b896c56 | ||
|
|
2e95608b05 | ||
|
|
d393c8c175 | ||
|
|
668e240e12 | ||
|
|
0831d37d3c | ||
|
|
60dff74507 | ||
|
|
f995440098 | ||
|
|
7a5ee1aa5b | ||
|
|
3639527b0e | ||
|
|
f4816124c2 | ||
|
|
664b50d4ef | ||
|
|
1768f5a38e | ||
|
|
f2453ba0a3 | ||
|
|
7b769eeb24 | ||
|
|
391e0c8a6f | ||
|
|
0149e06c42 | ||
|
|
832b362254 | ||
|
|
eee9ba1c13 | ||
|
|
aaecffbdfa | ||
|
|
917c10c1d3 | ||
|
|
14a0d65a01 | ||
|
|
468d436d3e | ||
|
|
c85a82b9ac | ||
|
|
3de5215b5e | ||
|
|
5ffd471a45 | ||
|
|
1da6fbd1c4 | ||
|
|
5204536f92 | ||
|
|
7a84cd9162 | ||
|
|
4f26ddd2cc | ||
|
|
b7859d4ede | ||
|
|
cbdca71e3f | ||
|
|
fd31609cb3 | ||
|
|
009519ee62 | ||
|
|
b6f9b5a3a4 | ||
|
|
7387a2b56e | ||
|
|
9ff103d4d0 | ||
|
|
32418b42a0 | ||
|
|
bf2208e77b | ||
|
|
5702d45bc6 | ||
|
|
42352b4c2d | ||
|
|
729416e5ef | ||
|
|
41b3be926a | ||
|
|
fc67e7cf0b | ||
|
|
9b3c06fc6f | ||
|
|
f0e3c4812b | ||
|
|
4c7d8c6f2e | ||
|
|
74dd8e3691 | ||
|
|
7782d93fe9 | ||
|
|
fd26b0ee81 | ||
|
|
a01902356d | ||
|
|
0692cb3aea | ||
|
|
de3f067372 | ||
|
|
a0c11e4bb7 | ||
|
|
71e7fac555 | ||
|
|
6dc27a355f | ||
|
|
eeec725f08 | ||
|
|
97df3193e3 | ||
|
|
ed2d67934c | ||
|
|
1a959c2ce8 | ||
|
|
88eda43560 | ||
|
|
133e53cfa6 | ||
|
|
c69f4f7fc7 | ||
|
|
10f993e686 | ||
|
|
a02ddfbfbc | ||
|
|
7a85c93631 | ||
|
|
a3d11cfca3 | ||
|
|
74279fc443 | ||
|
|
7e3a72961f | ||
|
|
ca2179352c | ||
|
|
8ddac065c6 | ||
|
|
a930bd35a5 | ||
|
|
6b8f3ff806 | ||
|
|
3323f392e3 | ||
|
|
f8f2b80641 | ||
|
|
1729443cc3 | ||
|
|
6b5ac8a8b3 | ||
|
|
b12aca2757 | ||
|
|
39cc3a780a | ||
|
|
18302723e8 | ||
|
|
20c383d22d | ||
|
|
77a0c4e018 | ||
|
|
9a5c8b193d | ||
|
|
0562ba8ab0 | ||
|
|
906e860c86 | ||
|
|
516e1fb58d | ||
|
|
7da1307cf6 | ||
|
|
ea4893d815 | ||
|
|
f2e0079b59 | ||
|
|
47d8baa496 | ||
|
|
6d96d64811 | ||
|
|
92fb998e5a | ||
|
|
bed6030e2b | ||
|
|
328a85bad1 | ||
|
|
394375e429 | ||
|
|
ca744adfd9 | ||
|
|
5e42f55570 | ||
|
|
021da1a074 | ||
|
|
0e55f45e91 | ||
|
|
9749837506 | ||
|
|
4fbb0b98c3 | ||
|
|
0edf8afcaf | ||
|
|
e2ee54578c | ||
|
|
b35e4484de | ||
|
|
c8368612eb | ||
|
|
26266e724b | ||
|
|
6ccfea9eb4 | ||
|
|
d4349c20a7 | ||
|
|
63f1cc2450 | ||
|
|
acec2a694a | ||
|
|
f15d7224e0 | ||
|
|
236f552a22 | ||
|
|
321b6f2c64 | ||
|
|
218ead9a31 | ||
|
|
60b6e82c88 | ||
|
|
bbe4ad938a | ||
|
|
9e27380a10 | ||
|
|
baf83fd079 | ||
|
|
dfe3163692 | ||
|
|
4452bd4088 | ||
|
|
6f93b840a9 | ||
|
|
443f50c144 | ||
|
|
5a7f902d01 | ||
|
|
02cc82cee1 | ||
|
|
fa9b05958d | ||
|
|
6fb2c9e7df | ||
|
|
54e59b4b3e | ||
|
|
8a0ea6cae5 | ||
|
|
1bbd816e83 | ||
|
|
4e56191b7e | ||
|
|
4404fb49d3 | ||
|
|
fd4db442e4 | ||
|
|
416f392108 | ||
|
|
dbe9f6b0b2 | ||
|
|
18f06f7a63 | ||
|
|
fb75ddc3e9 | ||
|
|
1326a40033 | ||
|
|
85be8d2ce7 | ||
|
|
b5fb8c8cac | ||
|
|
71b7ec4987 | ||
|
|
ad110d46d9 | ||
|
|
dabe0c13ef | ||
|
|
a5dbfb5cba | ||
|
|
75c4700ac6 | ||
|
|
19ce60fd0a | ||
|
|
d26933b722 | ||
|
|
30dfe2c0c0 | ||
|
|
fa55bcd5f3 | ||
|
|
fbc1dc9406 | ||
|
|
4a4b82c8b6 | ||
|
|
042e477252 | ||
|
|
d1d7b72441 | ||
|
|
48d8d7f527 | ||
|
|
73a71e5262 | ||
|
|
cab779e403 | ||
|
|
e51e874264 | ||
|
|
e399c9f974 | ||
|
|
acd46e5f2a | ||
|
|
59af8453f3 | ||
|
|
d03f4d9aae | ||
|
|
4cc2c3310c | ||
|
|
1123b34e10 | ||
|
|
8c12fce187 | ||
|
|
a82176c855 | ||
|
|
7180bbd6d4 | ||
|
|
b9b4539764 | ||
|
|
96ccc01d08 | ||
|
|
2108dd6c94 | ||
|
|
ca2003f2d6 | ||
|
|
acbbac588f | ||
|
|
f75ff97042 | ||
|
|
4aa7af9716 | ||
|
|
d2682da3cc | ||
|
|
aa1d2ab623 | ||
|
|
929333d264 | ||
|
|
d2cbf823d8 | ||
|
|
ba580e43e6 | ||
|
|
3c49cec213 | ||
|
|
c237fce38d | ||
|
|
8acecc61cd | ||
|
|
63149b7597 | ||
|
|
a331eeadcb | ||
|
|
33e109b131 | ||
|
|
68b4328c72 | ||
|
|
dd8a0a1952 | ||
|
|
b58c1eb9ec | ||
|
|
27994d6ac6 | ||
|
|
9d6978e9c7 | ||
|
|
62ae4bc890 | ||
|
|
c2ce6a859b | ||
|
|
aa5e0a5507 | ||
|
|
6c1c1def78 | ||
|
|
1a232183a8 | ||
|
|
cfa7d5be69 | ||
|
|
bb375eb149 | ||
|
|
7e5e6ce680 | ||
|
|
1032f8b4f1 | ||
|
|
744a56c8b2 | ||
|
|
e58372a190 | ||
|
|
19d44b8666 | ||
|
|
92a52d6194 | ||
|
|
db1c0e25c0 | ||
|
|
04860a7304 | ||
|
|
d8d67301ed | ||
|
|
fa1e915b9c | ||
|
|
da86b69b2c | ||
|
|
44e3167278 | ||
|
|
5cc22f14bc | ||
|
|
8519af5cbb | ||
|
|
3fc4be8335 | ||
|
|
5a5cc0bed3 | ||
|
|
1a9e7503ef | ||
|
|
d84e9e3406 | ||
|
|
c41954993c | ||
|
|
a7c8fc3bff | ||
|
|
36c1f10de8 | ||
|
|
6002f97fb6 | ||
|
|
4378dad2b9 | ||
|
|
278298ecd2 | ||
|
|
b8959cb7bb | ||
|
|
dec90bbbbf | ||
|
|
a4b9d2f04d | ||
|
|
ee41366c08 | ||
|
|
3f42b77d13 | ||
|
|
e22f78a449 | ||
|
|
f2b66ff19f | ||
|
|
2fd0893773 | ||
|
|
0e9292d6d9 | ||
|
|
69250ada66 | ||
|
|
22f8b5f203 | ||
|
|
987acc5b0f | ||
|
|
ce844137a0 | ||
|
|
7f3a8c07e2 | ||
|
|
bd14d58f08 | ||
|
|
b8494cd1d9 | ||
|
|
1a1c3db8e5 | ||
|
|
49772c1334 | ||
|
|
02ddb99000 | ||
|
|
cf1509e34f | ||
|
|
ce2e671768 | ||
|
|
cf2add29dd | ||
|
|
8d2bd958ab | ||
|
|
fcdd8ecbfa | ||
|
|
58beb0d5c5 | ||
|
|
2b1e33b89a | ||
|
|
02d77847da | ||
|
|
ad999b7e71 | ||
|
|
06540f5342 | ||
|
|
f1a34abe87 | ||
|
|
18c3248113 | ||
|
|
aacc39810b | ||
|
|
24f19b8798 | ||
|
|
ff17f4f740 | ||
|
|
2db4497385 | ||
|
|
94036a31e5 | ||
|
|
866baf9f70 | ||
|
|
90cdc55574 | ||
|
|
2f562e82a0 | ||
|
|
e6adc3876e | ||
|
|
475a7504a5 | ||
|
|
7c04622019 | ||
|
|
5609b28e5a | ||
|
|
2dc2a49758 | ||
|
|
e304c66f93 | ||
|
|
db0caf1bea | ||
|
|
8ddb87ddca | ||
|
|
730ddb1c96 | ||
|
|
48f428a4f0 | ||
|
|
806db63e9e | ||
|
|
c2a30c46c3 | ||
|
|
1ce08a7f32 | ||
|
|
4486d26e50 | ||
|
|
e49a3b46e6 | ||
|
|
7bfb341551 | ||
|
|
05e1dae033 | ||
|
|
7f98454965 | ||
|
|
c6ec485303 | ||
|
|
d6d3e3b0e9 | ||
|
|
91b6d6a9dc | ||
|
|
b2f6165ae3 | ||
|
|
4ab49794de | ||
|
|
fe69642a64 | ||
|
|
65829f5e23 | ||
|
|
8713bf8646 | ||
|
|
3ec0e1f992 | ||
|
|
74151c65d0 | ||
|
|
e16777f95c | ||
|
|
8e9e66f846 | ||
|
|
0e9db3ee20 | ||
|
|
220013c4f0 | ||
|
|
d23d5da165 | ||
|
|
d8a2789760 | ||
|
|
53315bbb56 | ||
|
|
c88f5442ab | ||
|
|
7f347b6c90 | ||
|
|
e607ac8232 | ||
|
|
79b4441ffc | ||
|
|
2594cd475f | ||
|
|
74b808ada0 | ||
|
|
4fdbd22879 | ||
|
|
0392672a2d | ||
|
|
3d229a2de2 | ||
|
|
7e090ab405 | ||
|
|
b8c3758714 | ||
|
|
298fc0e04b | ||
|
|
1c1bffaee4 | ||
|
|
14506e545a | ||
|
|
89dff75416 | ||
|
|
8a790582f4 | ||
|
|
e6aff8f512 | ||
|
|
1c277d1944 | ||
|
|
26cc166282 | ||
|
|
30e878979c | ||
|
|
0f335e0984 | ||
|
|
4083c05b72 | ||
|
|
7b4d24a212 | ||
|
|
02c31c8bb0 | ||
|
|
6c0278c6e9 | ||
|
|
41e0320ce1 | ||
|
|
d97b02bace | ||
|
|
11bbe953f3 | ||
|
|
ecdd895a5a | ||
|
|
07bb815754 | ||
|
|
2a64fcdf19 | ||
|
|
f8b7bda74a | ||
|
|
5a0fc5f36b | ||
|
|
9e378b32ac | ||
|
|
968587d847 | ||
|
|
fa809ccd0b | ||
|
|
a8800a3be1 | ||
|
|
1f23b58d1e | ||
|
|
c6b266b0de | ||
|
|
3eba32c235 | ||
|
|
ec76ae426e | ||
|
|
e35d59c652 | ||
|
|
73fc89557b | ||
|
|
be98715045 | ||
|
|
4392b7915f | ||
|
|
b1fb92549c | ||
|
|
9bba666f3a | ||
|
|
dcf0fd61ba | ||
|
|
3d02146911 | ||
|
|
10bc822233 | ||
|
|
baac1a243a | ||
|
|
7c233d7c03 | ||
|
|
7c19dbddf0 | ||
|
|
f4b248573a | ||
|
|
14e454e9d9 | ||
|
|
ebab7d8804 | ||
|
|
78a3fb1879 | ||
|
|
d42b58c4fe | ||
|
|
724ad8e7a1 | ||
|
|
2fe50729a5 | ||
|
|
eb4eee1aef | ||
|
|
db972e2923 | ||
|
|
00817925e4 | ||
|
|
347ab3c811 | ||
|
|
a3d05b15db | ||
|
|
9093d0d601 | ||
|
|
8c57b98627 | ||
|
|
4b99751c0e | ||
|
|
66f4331b4e | ||
|
|
e1a824d565 | ||
|
|
883a88a551 | ||
|
|
a328bc3767 | ||
|
|
255447f788 | ||
|
|
b3d57ca717 | ||
|
|
45d21c4372 | ||
|
|
7f7b5ae082 | ||
|
|
028d8f1fc8 | ||
|
|
8d01ca1363 | ||
|
|
a36403d279 | ||
|
|
a78ad69c0c | ||
|
|
22c856afb6 | ||
|
|
71a073ebf9 | ||
|
|
f709cac95e | ||
|
|
27867e48df | ||
|
|
f1a7e1dfcd | ||
|
|
31e9566f2d | ||
|
|
f7f749e230 | ||
|
|
f5b7da7b0a | ||
|
|
2311188b13 | ||
|
|
25637b52d9 | ||
|
|
fddcc86cd6 | ||
|
|
c1b2be7d70 | ||
|
|
0a7b3a36aa | ||
|
|
57f102de71 | ||
|
|
6b22887a90 | ||
|
|
3b3f212ceb | ||
|
|
1a11e31836 | ||
|
|
d4b69c3990 | ||
|
|
4cbccd533a | ||
|
|
a9bc3ab0fc | ||
|
|
d9f929df04 | ||
|
|
bc154a4e6d | ||
|
|
7d48e40e05 | ||
|
|
47af253bf8 | ||
|
|
e126dfccf9 | ||
|
|
be5191825d | ||
|
|
68cf9c4c06 | ||
|
|
e46b7ae58d | ||
|
|
3d81fb8dd7 | ||
|
|
ff983dca60 | ||
|
|
fb0fceb6c4 | ||
|
|
9591afb9a2 | ||
|
|
0afde8ac8e | ||
|
|
4a83486b30 | ||
|
|
a3ceac1636 | ||
|
|
8666d17a42 | ||
|
|
96d13b2400 | ||
|
|
12fc1f80cc | ||
|
|
2dfe4e9b37 | ||
|
|
46c634e5a7 | ||
|
|
880680d7f8 | ||
|
|
7d5ed772c2 | ||
|
|
83fbcdeaa8 | ||
|
|
55a761d3a5 | ||
|
|
abe8e7750f | ||
|
|
aeef75b831 | ||
|
|
d6356d0244 | ||
|
|
bbf2aeb95f | ||
|
|
3a7d78ae05 | ||
|
|
2a2f752692 | ||
|
|
8883bf8a07 | ||
|
|
6ec80e2796 | ||
|
|
849a7b77cf | ||
|
|
36ccd0c381 | ||
|
|
58e7c8d6c9 | ||
|
|
d04fc8cc0f | ||
|
|
339a1b1452 | ||
|
|
2d2d63d09e | ||
|
|
8241f7fd2f | ||
|
|
985571af87 | ||
|
|
82e1e4a466 | ||
|
|
9a7f7bbb78 | ||
|
|
359f29b75e | ||
|
|
ceced7cbaa | ||
|
|
6b5cd00b10 | ||
|
|
02eae47d34 | ||
|
|
3f01d38676 | ||
|
|
390a29ef55 | ||
|
|
b7f5786747 | ||
|
|
814c535757 | ||
|
|
ed0e27e4fd | ||
|
|
659df480d7 | ||
|
|
d0c45f6180 | ||
|
|
5a20461bd4 | ||
|
|
849f52fb8e | ||
|
|
da0c699b88 | ||
|
|
af5514c982 | ||
|
|
cc689e80a9 | ||
|
|
3b51374678 | ||
|
|
f07569db98 | ||
|
|
c5fcb90683 | ||
|
|
270f87dee1 | ||
|
|
765b8f3b35 | ||
|
|
3ff695899c | ||
|
|
be35baa838 | ||
|
|
470d4d7314 | ||
|
|
0976608a25 | ||
|
|
e7f4dfa07e | ||
|
|
2c68f335c6 | ||
|
|
cf29589e29 | ||
|
|
05cabd8edc | ||
|
|
2f4ecba51a | ||
|
|
74a7c9f23f | ||
|
|
b075a717f7 | ||
|
|
a3dcb888d3 | ||
|
|
02706af508 | ||
|
|
1f2c007776 | ||
|
|
42dfdfef13 | ||
|
|
b40bf0b9e1 | ||
|
|
cc15a9c468 | ||
|
|
5d8b1eecb4 | ||
|
|
093d82d762 | ||
|
|
686577c209 | ||
|
|
c2b333a78b | ||
|
|
cef8b8bf0f | ||
|
|
62a46710ba | ||
|
|
a3e6feb07e | ||
|
|
2ad093d4f1 | ||
|
|
6017d576fc | ||
|
|
08ac38dc87 | ||
|
|
3960daa556 | ||
|
|
395cbd87dd | ||
|
|
23429c047f | ||
|
|
af68748c16 | ||
|
|
63942045be | ||
|
|
a4cdc6e4f3 | ||
|
|
07c611bdbe |
@@ -0,0 +1,58 @@
|
|||||||
|
# Default branch protection
|
||||||
|
|
||||||
|
The intended repository ruleset is stored in
|
||||||
|
[`rulesets/protect-default-branch.json`](rulesets/protect-default-branch.json).
|
||||||
|
It targets the configured default branch and requires pull requests, the strict
|
||||||
|
`Required CI` check from GitHub Actions, resolved review conversations,
|
||||||
|
squash-only merges, and linear history. Branch deletion and force pushes are
|
||||||
|
blocked. Qodana remains advisory and is not part of the required gate.
|
||||||
|
|
||||||
|
The ruleset's `RepositoryRole` actor ID `5` is GitHub's built-in Administrator
|
||||||
|
role. Its `pull_request` bypass mode permits an administrator to bypass rules
|
||||||
|
only while merging an existing pull request; it does not permit a direct push.
|
||||||
|
|
||||||
|
## Repository settings
|
||||||
|
|
||||||
|
Keep squash merge enabled and disable merge commits and rebase merge. Enable
|
||||||
|
auto-merge, the update-branch option, and automatic deletion of merged head
|
||||||
|
branches. Keep the Actions token read-only and do not allow Actions to approve
|
||||||
|
pull-request reviews.
|
||||||
|
|
||||||
|
## Activation and verification
|
||||||
|
|
||||||
|
1. Confirm a pull request and a `master` push each produce exactly one
|
||||||
|
successful `Required CI` check from GitHub Actions integration `15368`.
|
||||||
|
2. For the initial ruleset POST, override the committed JSON's `enforcement`
|
||||||
|
value to `disabled`, then compare GitHub's normalized API response with this
|
||||||
|
file.
|
||||||
|
3. PUT the exact committed JSON to the inspected ruleset to activate it.
|
||||||
|
4. Open a canary pull request and confirm that pending or failing CI, unresolved
|
||||||
|
conversations, and an out-of-date branch block merging; only squash merge is
|
||||||
|
available.
|
||||||
|
5. After merging, confirm the head branch is deleted and the post-merge full
|
||||||
|
E2E, frontend release, and mobile release guards still run.
|
||||||
|
|
||||||
|
If validation exposes a blocker, disable the ruleset rather than deleting it so
|
||||||
|
its configuration and history remain available.
|
||||||
|
|
||||||
|
## Activation record
|
||||||
|
|
||||||
|
Repository ruleset `19051697` was activated on 2026-07-16 after preparation
|
||||||
|
PR #172 established `Required CI`, remediation PR #174 passed the strict
|
||||||
|
current-base gate, and merged master run `29501928124` completed with all 43
|
||||||
|
executed jobs successful. This documentation update is the after-activation
|
||||||
|
canary for the normal protected pull-request path.
|
||||||
|
|
||||||
|
## Normal publishing flow
|
||||||
|
|
||||||
|
Create a scoped feature branch, open a pull request to `master`, wait for
|
||||||
|
`Required CI`, update the branch if `master` advanced, resolve every review
|
||||||
|
conversation, and squash-merge. For waits expected to exceed 90 seconds, use
|
||||||
|
the workspace `scripts/ci-watch.sh` helper instead of repeatedly polling GitHub.
|
||||||
|
|
||||||
|
## Break glass
|
||||||
|
|
||||||
|
For an incident, an administrator must still open a pull request. Document the
|
||||||
|
incident and why the normal gate cannot complete, then use the PR-only bypass
|
||||||
|
when merging. Monitor all post-merge workflows and open a follow-up pull request
|
||||||
|
for any validation or remediation deferred during the incident.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
day: monday
|
||||||
|
time: "07:00"
|
||||||
|
timezone: Europe/Copenhagen
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
labels: [dependencies, ci]
|
||||||
|
|
||||||
|
- package-ecosystem: bundler
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
day: monday
|
||||||
|
time: "07:15"
|
||||||
|
timezone: Europe/Copenhagen
|
||||||
|
open-pull-requests-limit: 3
|
||||||
|
labels: [dependencies, ios]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "Protect default branch",
|
||||||
|
"target": "branch",
|
||||||
|
"enforcement": "active",
|
||||||
|
"bypass_actors": [
|
||||||
|
{
|
||||||
|
"actor_id": 5,
|
||||||
|
"actor_type": "RepositoryRole",
|
||||||
|
"bypass_mode": "pull_request"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"conditions": {
|
||||||
|
"ref_name": {
|
||||||
|
"include": ["~DEFAULT_BRANCH"],
|
||||||
|
"exclude": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "deletion"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "non_fast_forward"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "required_linear_history"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pull_request",
|
||||||
|
"parameters": {
|
||||||
|
"allowed_merge_methods": ["squash"],
|
||||||
|
"dismiss_stale_reviews_on_push": false,
|
||||||
|
"require_code_owner_review": false,
|
||||||
|
"require_last_push_approval": false,
|
||||||
|
"required_approving_review_count": 0,
|
||||||
|
"required_review_thread_resolution": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "required_status_checks",
|
||||||
|
"parameters": {
|
||||||
|
"do_not_enforce_on_create": false,
|
||||||
|
"required_status_checks": [
|
||||||
|
{
|
||||||
|
"context": "Required CI",
|
||||||
|
"integration_id": 15368
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strict_required_status_checks_policy": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
name: App Store Readiness
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- "fastlane/**"
|
||||||
|
- "ios/**"
|
||||||
|
- "scripts/mobile/**"
|
||||||
|
- "tests/node/app-store-connect.test.mjs"
|
||||||
|
- ".github/workflows/app-store-readiness.yml"
|
||||||
|
- "Gemfile*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: app-store-readiness-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
name: App Store Readiness
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Setup Ruby
|
||||||
|
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
|
||||||
|
with:
|
||||||
|
ruby-version: "3.3"
|
||||||
|
|
||||||
|
- name: Resolve the pinned Fastlane dependency graph
|
||||||
|
run: bundle lock
|
||||||
|
|
||||||
|
- name: Check the committed Fastlane dependency lock
|
||||||
|
id: fastlane-lock
|
||||||
|
continue-on-error: true
|
||||||
|
run: test -z "$(git status --porcelain -- Gemfile.lock)"
|
||||||
|
|
||||||
|
- name: Preserve a generated lock for review
|
||||||
|
if: steps.fastlane-lock.outcome == 'failure'
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: generated-fastlane-lock
|
||||||
|
path: Gemfile.lock
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
- name: Require a current committed Fastlane dependency lock
|
||||||
|
if: steps.fastlane-lock.outcome == 'failure'
|
||||||
|
run: |
|
||||||
|
echo 'Gemfile.lock is missing or stale. Download generated-fastlane-lock and commit it.' >&2
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Install the pinned Fastlane dependency graph
|
||||||
|
run: bundle install --jobs 4 --retry 3
|
||||||
|
|
||||||
|
- name: Validate strict App Store metadata and candidate assets
|
||||||
|
run: node scripts/mobile/validate-app-store.mjs --strict
|
||||||
|
|
||||||
|
- name: Validate native mobile permissions
|
||||||
|
run: node scripts/mobile/check-permissions.mjs
|
||||||
|
|
||||||
|
- name: Test App Store Connect automation
|
||||||
|
run: node --test tests/node/app-store-connect.test.mjs
|
||||||
|
|
||||||
|
- name: Validate Fastlane configuration
|
||||||
|
run: bundle exec fastlane lanes
|
||||||
|
|
||||||
|
- name: Validate JavaScript syntax
|
||||||
|
run: |
|
||||||
|
node --check scripts/mobile/validate-app-store.mjs
|
||||||
|
node --check scripts/mobile/app-store-connect.mjs
|
||||||
|
node --check scripts/mobile/create-ios-release-manifest.mjs
|
||||||
|
node --check tests/node/app-store-connect.test.mjs
|
||||||
|
node scripts/mobile/app-store-connect.mjs self-test-jwt
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
name: Qodana Configuration Upload
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, dev]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
upload-qodana-config:
|
|
||||||
runs-on: [self-hosted, Linux, X64, default]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Run Qodana Configuration Uploader
|
|
||||||
env:
|
|
||||||
QODANA_CONFIGURATIONS_TOKEN: ${{ secrets.QODANA_CONFIGURATIONS_TOKEN }}
|
|
||||||
run: |
|
|
||||||
docker run --rm \
|
|
||||||
-v $(pwd):/workspace \
|
|
||||||
-w /workspace \
|
|
||||||
-e QODANA_CONFIGURATIONS_TOKEN=$QODANA_CONFIGURATIONS_TOKEN \
|
|
||||||
jetbrains/qodana-configuration-uploader:latest \
|
|
||||||
--global-configs-file qodana-global-configurations.yaml \
|
|
||||||
--qodana-host https://qodana.cloud
|
|
||||||
@@ -1,28 +1,66 @@
|
|||||||
name: Qodana Configuration Upload
|
name: Qodana
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches: [main, dev]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches: [master, beta, canary, internal]
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
push:
|
||||||
|
branches: [master, beta, canary, internal]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
checks: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: qodana-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
upload-qodana-config:
|
qodana:
|
||||||
runs-on: [self-hosted, Linux, X64, default]
|
name: Qodana
|
||||||
|
if: >-
|
||||||
|
github.event_name != 'pull_request' ||
|
||||||
|
(
|
||||||
|
github.event.pull_request.draft == false &&
|
||||||
|
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||||
|
github.event.pull_request.user.login != 'dependabot[bot]'
|
||||||
|
)
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 60
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
# v5.0.1
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run Qodana Configuration Uploader
|
- name: Require Qodana project token
|
||||||
|
shell: bash
|
||||||
env:
|
env:
|
||||||
QODANA_CONFIGURATIONS_TOKEN: ${{ secrets.QODANA_CONFIGURATIONS_TOKEN }}
|
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
docker run --rm \
|
set -euo pipefail
|
||||||
-v $(pwd):/workspace \
|
if [[ -z "${QODANA_TOKEN:-}" ]]; then
|
||||||
-w /workspace \
|
echo "::error::QODANA_TOKEN is not configured for this repository."
|
||||||
-e QODANA_CONFIGURATIONS_TOKEN=$QODANA_CONFIGURATIONS_TOKEN \
|
exit 1
|
||||||
jetbrains/qodana-configuration-uploader:latest \
|
fi
|
||||||
--global-configs-file qodana-global-configurations.yaml \
|
|
||||||
--qodana-host https://qodana.cloud
|
- name: Qodana
|
||||||
|
# v2026.1.3
|
||||||
|
uses: JetBrains/qodana-action@b588768b6e7e6da579e518bc584f79de0d243692
|
||||||
|
with:
|
||||||
|
use-caches: true
|
||||||
|
cache-default-branch-only: true
|
||||||
|
upload-result: false
|
||||||
|
use-annotations: true
|
||||||
|
pr-mode: ${{ github.event_name == 'pull_request' }}
|
||||||
|
post-pr-comment: true
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
push-fixes: none
|
||||||
|
env:
|
||||||
|
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
name: iOS App Store Candidate
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["ios-v*"]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ios-app-store-candidate
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
resolve:
|
||||||
|
name: Resolve exact tested build
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 15
|
||||||
|
outputs:
|
||||||
|
enabled: ${{ steps.resolve.outputs.enabled }}
|
||||||
|
source_sha: ${{ steps.resolve.outputs.source_sha }}
|
||||||
|
version: ${{ steps.resolve.outputs.version }}
|
||||||
|
build_number: ${{ steps.manifest.outputs.build_number }}
|
||||||
|
app_store_build_id: ${{ steps.manifest.outputs.app_store_build_id }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout tagged source
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Validate protected tag and release version
|
||||||
|
id: resolve
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
AUTOMATION_ENABLED: ${{ vars.APP_STORE_AUTOMATION_ENABLED || 'false' }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
[[ "$GITHUB_REF_NAME" =~ ^ios-v([0-9]+\.[0-9]+\.[0-9]+)$ ]] || { echo "Tag must be ios-vX.Y.Z." >&2; exit 1; }
|
||||||
|
version="${BASH_REMATCH[1]}"
|
||||||
|
source_sha="$(git rev-parse HEAD)"
|
||||||
|
manifest_version="$(node -p "JSON.parse(require('fs').readFileSync('ios/release.json')).marketingVersion")"
|
||||||
|
[[ "$version" == "$manifest_version" ]] || { echo "Tag version $version does not match ios/release.json $manifest_version." >&2; exit 1; }
|
||||||
|
git show-ref --verify --quiet refs/remotes/origin/master || { echo "origin/master was not included in the full checkout." >&2; exit 1; }
|
||||||
|
git merge-base --is-ancestor "$source_sha" origin/master || { echo "Tagged commit is not reachable from master." >&2; exit 1; }
|
||||||
|
enabled=false
|
||||||
|
[[ "$AUTOMATION_ENABLED" == true ]] && enabled=true
|
||||||
|
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
if [[ "$enabled" != true ]]; then
|
||||||
|
echo "### Candidate promotion safely disabled" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo 'No App Store environment or credentials were accessed. Enable only after the signed canary.' >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Download exact TestFlight release manifest
|
||||||
|
if: steps.resolve.outputs.enabled == 'true'
|
||||||
|
id: manifest
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
SOURCE_SHA: ${{ steps.resolve.outputs.source_sha }}
|
||||||
|
EXPECTED_VERSION: ${{ steps.resolve.outputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
artifact_name="ios-release-manifest-$SOURCE_SHA"
|
||||||
|
response="$RUNNER_TEMP/ios-artifacts.json"
|
||||||
|
curl --fail --silent --show-error --location \
|
||||||
|
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||||
|
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts?name=$artifact_name&per_page=100" > "$response"
|
||||||
|
artifact_id="$(jq -r --arg sha "$SOURCE_SHA" '[.artifacts[] | select(.expired == false) | select(.workflow_run.head_sha == $sha)] | sort_by(.created_at) | last | .id // empty' "$response")"
|
||||||
|
[[ "$artifact_id" =~ ^[0-9]+$ ]] || { echo "No successful TestFlight release manifest exists for $SOURCE_SHA." >&2; exit 1; }
|
||||||
|
mkdir -p output/candidate
|
||||||
|
curl --fail --silent --show-error --location \
|
||||||
|
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||||
|
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id/zip" -o "$RUNNER_TEMP/manifest.zip"
|
||||||
|
unzip -q "$RUNNER_TEMP/manifest.zip" -d output/candidate
|
||||||
|
MANIFEST=output/candidate/ios-release-manifest.json node <<'NODE'
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(process.env.MANIFEST, "utf8"));
|
||||||
|
const checks = {
|
||||||
|
schema: manifest.schemaVersion === 1,
|
||||||
|
repository: manifest.repository === process.env.GITHUB_REPOSITORY,
|
||||||
|
source: manifest.sourceSha === process.env.SOURCE_SHA,
|
||||||
|
version: manifest.marketingVersion === process.env.EXPECTED_VERSION,
|
||||||
|
bundle: manifest.bundleId === "io.truckwash.app",
|
||||||
|
build: /^[1-9][0-9]*$/.test(manifest.buildNumber),
|
||||||
|
appStoreBuild: typeof manifest.appStoreBuildId === "string" && manifest.appStoreBuildId.length > 0,
|
||||||
|
};
|
||||||
|
const failed = Object.entries(checks).filter(([, ok]) => !ok).map(([name]) => name);
|
||||||
|
if (failed.length) throw new Error(`Invalid iOS release manifest: ${failed.join(", ")}`);
|
||||||
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `build_number=${manifest.buildNumber}\napp_store_build_id=${manifest.appStoreBuildId}\n`);
|
||||||
|
NODE
|
||||||
|
|
||||||
|
promote:
|
||||||
|
name: Sync and verify App Store candidate
|
||||||
|
needs: resolve
|
||||||
|
if: needs.resolve.outputs.enabled == 'true'
|
||||||
|
runs-on: macos-15
|
||||||
|
timeout-minutes: 60
|
||||||
|
environment: app-store-candidate
|
||||||
|
env:
|
||||||
|
IOS_SOURCE_SHA: ${{ needs.resolve.outputs.source_sha }}
|
||||||
|
IOS_MARKETING_VERSION: ${{ needs.resolve.outputs.version }}
|
||||||
|
IOS_BUILD_NUMBER: ${{ needs.resolve.outputs.build_number }}
|
||||||
|
EXPECTED_APP_STORE_BUILD_ID: ${{ needs.resolve.outputs.app_store_build_id }}
|
||||||
|
IOS_BUNDLE_ID: ${{ vars.IOS_BUNDLE_ID || 'io.truckwash.app' }}
|
||||||
|
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||||
|
APP_STORE_CONNECT_API_KEY_ID: ${{ vars.APP_STORE_CONNECT_API_KEY_ID }}
|
||||||
|
APP_STORE_CONNECT_ISSUER_ID: ${{ vars.APP_STORE_CONNECT_ISSUER_ID || '' }}
|
||||||
|
APP_STORE_CONNECT_APP_ID: ${{ vars.APP_STORE_CONNECT_APP_ID }}
|
||||||
|
APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout exact candidate source
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
ref: ${{ env.IOS_SOURCE_SHA }}
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Ruby and pinned Fastlane
|
||||||
|
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
|
||||||
|
with:
|
||||||
|
ruby-version: "3.3"
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Validate complete candidate storefront
|
||||||
|
run: node scripts/mobile/validate-app-store.mjs --strict
|
||||||
|
|
||||||
|
- name: Verify public storefront URLs
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
for file in support_url privacy_url marketing_url; do
|
||||||
|
url="$(tr -d '\r\n' < "fastlane/metadata/da-DK/$file.txt")"
|
||||||
|
curl --fail --silent --show-error --location --connect-timeout 10 --max-time 30 --output /dev/null "$url"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Verify exact processed TestFlight build
|
||||||
|
run: node scripts/mobile/app-store-connect.mjs verify-candidate
|
||||||
|
|
||||||
|
- name: Sync metadata and screenshots without App Review submission
|
||||||
|
run: bundle exec fastlane ios prepare_candidate
|
||||||
|
|
||||||
|
- name: Configure automatic release after approval
|
||||||
|
run: node scripts/mobile/app-store-connect.mjs configure-release-policy
|
||||||
|
|
||||||
|
- name: Read back exact App Store candidate
|
||||||
|
id: readback
|
||||||
|
run: node scripts/mobile/app-store-connect.mjs verify-store-version
|
||||||
|
|
||||||
|
- name: Verify Denmark-only availability and no preorder
|
||||||
|
id: availability
|
||||||
|
run: node scripts/mobile/app-store-connect.mjs verify-availability
|
||||||
|
|
||||||
|
- name: Write candidate handoff
|
||||||
|
env:
|
||||||
|
APP_STORE_STATE: ${{ steps.readback.outputs.app_store_state }}
|
||||||
|
APP_STORE_VERSION_ID: ${{ steps.readback.outputs.app_store_version_id }}
|
||||||
|
RELEASE_TYPE: ${{ steps.readback.outputs.release_type }}
|
||||||
|
AVAILABLE_TERRITORIES: ${{ steps.availability.outputs.available_territories }}
|
||||||
|
run: |
|
||||||
|
echo "### iOS $IOS_MARKETING_VERSION candidate prepared" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "- Source: \`$IOS_SOURCE_SHA\`" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "- Exact tested build: \`$IOS_BUILD_NUMBER\` (\`$EXPECTED_APP_STORE_BUILD_ID\`)" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "- App Store state: \`$APP_STORE_STATE\`" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "- App Store version ID: \`$APP_STORE_VERSION_ID\`" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "- Release policy: \`$RELEASE_TYPE\`" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "- Availability: \`$AVAILABLE_TERRITORIES\` only; preorder disabled" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "- [Open the app in App Store Connect](https://appstoreconnect.apple.com/apps/$APP_STORE_CONNECT_APP_ID/appstore)" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "- App Review submission remains manual; Apple will release automatically after approval." >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
disabled:
|
||||||
|
name: Promotion disabled
|
||||||
|
needs: resolve
|
||||||
|
if: needs.resolve.outputs.enabled != 'true'
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- run: echo "App Store candidate promotion is disabled; no environment or credentials were accessed."
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
name: iOS Credential Health
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "17 6 * * 1"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ios-credential-health
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gate:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
outputs:
|
||||||
|
enabled: ${{ steps.gate.outputs.enabled }}
|
||||||
|
steps:
|
||||||
|
- id: gate
|
||||||
|
env:
|
||||||
|
ENABLED: ${{ vars.APP_STORE_AUTOMATION_ENABLED || 'false' }}
|
||||||
|
run: |
|
||||||
|
enabled=false
|
||||||
|
[[ "$ENABLED" == true ]] && enabled=true
|
||||||
|
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
|
||||||
|
if [[ "$enabled" != true ]]; then
|
||||||
|
echo "App Store automation is disabled; credential health did not access its environment." >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
validate:
|
||||||
|
needs: gate
|
||||||
|
if: needs.gate.outputs.enabled == 'true'
|
||||||
|
runs-on: macos-15
|
||||||
|
timeout-minutes: 15
|
||||||
|
environment: app-store-signing
|
||||||
|
env:
|
||||||
|
IOS_BUNDLE_ID: ${{ vars.IOS_BUNDLE_ID || 'io.truckwash.app' }}
|
||||||
|
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||||
|
APP_STORE_CONNECT_API_KEY_ID: ${{ vars.APP_STORE_CONNECT_API_KEY_ID }}
|
||||||
|
APP_STORE_CONNECT_ISSUER_ID: ${{ vars.APP_STORE_CONNECT_ISSUER_ID || '' }}
|
||||||
|
APP_STORE_CONNECT_APP_ID: ${{ vars.APP_STORE_CONNECT_APP_ID }}
|
||||||
|
APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Validate API key and app access
|
||||||
|
run: node scripts/mobile/app-store-connect.mjs verify-credentials
|
||||||
|
|
||||||
|
- name: Validate certificate and profile identity and expiry
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64 }}
|
||||||
|
IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
|
||||||
|
IOS_APP_STORE_PROFILE_BASE64: ${{ secrets.IOS_APP_STORE_PROFILE_BASE64 }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cert_p12="$RUNNER_TEMP/distribution.p12"
|
||||||
|
cert_pem="$RUNNER_TEMP/distribution.pem"
|
||||||
|
cert_der="$RUNNER_TEMP/distribution.der"
|
||||||
|
profile="$RUNNER_TEMP/distribution.mobileprovision"
|
||||||
|
profile_plist="$RUNNER_TEMP/distribution-profile.plist"
|
||||||
|
keychain="$RUNNER_TEMP/credential-health.keychain-db"
|
||||||
|
keychain_password="$(openssl rand -hex 24)"
|
||||||
|
node -e "const fs=require('fs');fs.writeFileSync(process.argv[1],Buffer.from(process.env.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64.replace(/\\s/g,''),'base64'))" "$cert_p12"
|
||||||
|
node -e "const fs=require('fs');fs.writeFileSync(process.argv[1],Buffer.from(process.env.IOS_APP_STORE_PROFILE_BASE64.replace(/\\s/g,''),'base64'))" "$profile"
|
||||||
|
chmod 600 "$cert_p12" "$profile"
|
||||||
|
security create-keychain -p "$keychain_password" "$keychain"
|
||||||
|
security unlock-keychain -p "$keychain_password" "$keychain"
|
||||||
|
security import "$cert_p12" -P "$IOS_DISTRIBUTION_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain"
|
||||||
|
security list-keychains -d user -s "$keychain"
|
||||||
|
security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" >/dev/null
|
||||||
|
security find-identity -v -p codesigning "$keychain" | grep -q 'Apple Distribution' || {
|
||||||
|
echo "Distribution P12 does not contain a usable private signing identity." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
openssl pkcs12 -in "$cert_p12" -clcerts -nokeys -passin env:IOS_DISTRIBUTION_CERTIFICATE_PASSWORD -out "$cert_pem"
|
||||||
|
openssl x509 -in "$cert_pem" -noout -subject -issuer -dates
|
||||||
|
openssl x509 -in "$cert_pem" -checkend 2592000 -noout || { echo "Distribution certificate expires within 30 days." >&2; exit 1; }
|
||||||
|
openssl x509 -in "$cert_pem" -outform DER -out "$cert_der"
|
||||||
|
security cms -D -i "$profile" > "$profile_plist"
|
||||||
|
CERT_DER="$cert_der" PROFILE_PLIST="$profile_plist" python3 <<'PY'
|
||||||
|
import datetime, hashlib, os, plistlib, sys
|
||||||
|
with open(os.environ["PROFILE_PLIST"], "rb") as handle: profile = plistlib.load(handle)
|
||||||
|
with open(os.environ["CERT_DER"], "rb") as handle: cert_sha = hashlib.sha1(handle.read()).hexdigest().upper()
|
||||||
|
expiration = profile.get("ExpirationDate")
|
||||||
|
if expiration and expiration.tzinfo is None: expiration = expiration.replace(tzinfo=datetime.timezone.utc)
|
||||||
|
warning = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=30)
|
||||||
|
ent = profile.get("Entitlements", {})
|
||||||
|
checks = {
|
||||||
|
"team": os.environ["APPLE_TEAM_ID"] in profile.get("TeamIdentifier", []),
|
||||||
|
"bundle": ent.get("application-identifier") == f'{os.environ["APPLE_TEAM_ID"]}.{os.environ["IOS_BUNDLE_ID"]}',
|
||||||
|
"distribution": ent.get("get-task-allow") is False and not profile.get("ProvisionedDevices"),
|
||||||
|
"profile expiry beyond 30 days": expiration is not None and expiration > warning,
|
||||||
|
"certificate belongs to profile": cert_sha in {hashlib.sha1(value).hexdigest().upper() for value in profile.get("DeveloperCertificates", [])},
|
||||||
|
}
|
||||||
|
failed = [name for name, ok in checks.items() if not ok]
|
||||||
|
if failed:
|
||||||
|
print("Credential health failed:", *[f"- {name}" for name in failed], sep="\n", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"Provisioning profile is healthy through {expiration.isoformat()}.")
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: Clean temporary credential files
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
security delete-keychain "$RUNNER_TEMP/credential-health.keychain-db" 2>/dev/null || true
|
||||||
|
rm -f "$RUNNER_TEMP"/distribution.{p12,pem,der,mobileprovision} "$RUNNER_TEMP/distribution-profile.plist"
|
||||||
@@ -0,0 +1,624 @@
|
|||||||
|
name: iOS Device Debug IPA
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
source_ref:
|
||||||
|
description: Same-repository branch, tag, or commit to build
|
||||||
|
required: true
|
||||||
|
default: master
|
||||||
|
type: string
|
||||||
|
expected_sha:
|
||||||
|
description: Full 40-character SHA that source_ref must resolve to
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
confirmation:
|
||||||
|
description: Type SIGN IOS DEBUG IPA
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ios-device-debug-${{ github.run_id }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
resolve:
|
||||||
|
name: Resolve and verify source
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 10
|
||||||
|
outputs:
|
||||||
|
source_sha: ${{ steps.resolve.outputs.source_sha }}
|
||||||
|
steps:
|
||||||
|
- name: Validate dispatch confirmation
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CONFIRMATION: ${{ inputs.confirmation }}
|
||||||
|
EXPECTED_SHA: ${{ inputs.expected_sha }}
|
||||||
|
SOURCE_REF: ${{ inputs.source_ref }}
|
||||||
|
WORKFLOW_REF: ${{ github.ref }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ "$WORKFLOW_REF" != "refs/heads/master" ]]; then
|
||||||
|
echo "The signing workflow must be dispatched from the master workflow ref" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$CONFIRMATION" != "SIGN IOS DEBUG IPA" ]]; then
|
||||||
|
echo "confirmation must exactly match SIGN IOS DEBUG IPA" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! "$EXPECTED_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
|
||||||
|
echo "expected_sha must be a full 40-character commit SHA" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -z "$SOURCE_REF" || "$SOURCE_REF" =~ [[:space:]] ]]; then
|
||||||
|
echo "source_ref must be non-empty and contain no whitespace" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$SOURCE_REF" == refs/pull/* || "$SOURCE_REF" == pull/* ]]; then
|
||||||
|
echo "Pull-request refs are not eligible for device-debug signing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Checkout same-repository history
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Resolve immutable commit
|
||||||
|
id: resolve
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
EXPECTED_SHA: ${{ inputs.expected_sha }}
|
||||||
|
SOURCE_REF: ${{ inputs.source_ref }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
expected_sha="${EXPECTED_SHA,,}"
|
||||||
|
|
||||||
|
if [[ "$SOURCE_REF" =~ ^[0-9a-fA-F]{7,40}$ ]]; then
|
||||||
|
candidate="$SOURCE_REF"
|
||||||
|
elif [[ "$SOURCE_REF" == refs/heads/* ]]; then
|
||||||
|
candidate="refs/remotes/origin/${SOURCE_REF#refs/heads/}"
|
||||||
|
elif [[ "$SOURCE_REF" == refs/tags/* ]]; then
|
||||||
|
candidate="$SOURCE_REF"
|
||||||
|
elif git show-ref --verify --quiet "refs/remotes/origin/$SOURCE_REF"; then
|
||||||
|
candidate="refs/remotes/origin/$SOURCE_REF"
|
||||||
|
elif git show-ref --verify --quiet "refs/tags/$SOURCE_REF"; then
|
||||||
|
candidate="refs/tags/$SOURCE_REF"
|
||||||
|
else
|
||||||
|
echo "source_ref does not identify a same-repository branch, tag, or fetched commit" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
source_sha="$(git rev-parse --verify "${candidate}^{commit}" 2>/dev/null || true)"
|
||||||
|
source_sha="${source_sha,,}"
|
||||||
|
if [[ ! "$source_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||||
|
echo "source_ref could not be resolved to a commit" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$source_sha" != "$expected_sha" ]]; then
|
||||||
|
echo "source_ref resolved to a SHA different from expected_sha" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
reachable=false
|
||||||
|
while IFS= read -r repository_ref; do
|
||||||
|
if git merge-base --is-ancestor "$source_sha" "$repository_ref" 2>/dev/null; then
|
||||||
|
reachable=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin refs/tags)
|
||||||
|
if [[ "$reachable" != true ]]; then
|
||||||
|
echo "The requested commit is not reachable from a same-repository branch or tag" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Resolved source_ref to $source_sha" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build development-signed IPA
|
||||||
|
needs: resolve
|
||||||
|
runs-on: macos-26
|
||||||
|
timeout-minutes: 90
|
||||||
|
environment:
|
||||||
|
name: mobile-device-debug
|
||||||
|
env:
|
||||||
|
IOS_PROJECT_PATH: ios/App/App.xcodeproj
|
||||||
|
IOS_SCHEME: App
|
||||||
|
IOS_DEBUG_BUNDLE_ID: ${{ vars.IOS_DEBUG_BUNDLE_ID || 'io.truckwash.app.debug' }}
|
||||||
|
IOS_DEBUG_API_URL: ${{ vars.IOS_DEBUG_API_URL || 'https://api-v2.truckwash.io/master/api' }}
|
||||||
|
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||||
|
MOBILE_VERSION_NAME: 0.0.${{ github.run_number }}
|
||||||
|
RESOLVED_SOURCE_SHA: ${{ needs.resolve.outputs.source_sha }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout resolved source
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.resolve.outputs.source_sha }}
|
||||||
|
fetch-depth: 1
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Verify runner and resolve build number
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
xcode_version_output="$(xcodebuild -version)"
|
||||||
|
IFS= read -r xcode_version <<< "$xcode_version_output"
|
||||||
|
xcode_major="$(awk '{split($2, version, "."); print version[1]}' <<< "$xcode_version")"
|
||||||
|
if [[ ! "$xcode_major" =~ ^[0-9]+$ ]] || (( xcode_major < 26 )); then
|
||||||
|
echo "Xcode 26 or newer is required; found $xcode_version" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
build_number="$((10#$GITHUB_RUN_NUMBER * 100 + 10#$GITHUB_RUN_ATTEMPT))"
|
||||||
|
if [[ ! "$build_number" =~ ^[1-9][0-9]{0,17}$ ]]; then
|
||||||
|
echo "Derived build number is outside Apple's supported integer format" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "MOBILE_VERSION_CODE=$build_number" >> "$GITHUB_ENV"
|
||||||
|
echo "XCODE_VERSION=$xcode_version" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
- name: Verify stable API and Capacitor iOS CORS
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
api_base="${IOS_DEBUG_API_URL%/}"
|
||||||
|
curl --fail --silent --show-error --location \
|
||||||
|
--connect-timeout 10 --max-time 20 \
|
||||||
|
--header 'Accept: application/json' \
|
||||||
|
--output /dev/null \
|
||||||
|
"$api_base/ping"
|
||||||
|
|
||||||
|
cors_headers="$RUNNER_TEMP/ios-debug-cors-headers.txt"
|
||||||
|
cors_body="$RUNNER_TEMP/ios-debug-cors-body.txt"
|
||||||
|
cors_status="$(curl --silent --show-error \
|
||||||
|
--connect-timeout 10 --max-time 20 \
|
||||||
|
--request OPTIONS \
|
||||||
|
--header 'Origin: capacitor://localhost' \
|
||||||
|
--header 'Access-Control-Request-Method: POST' \
|
||||||
|
--header 'Access-Control-Request-Headers: authorization,content-type' \
|
||||||
|
--dump-header "$cors_headers" \
|
||||||
|
--output "$cors_body" \
|
||||||
|
--write-out '%{http_code}' \
|
||||||
|
"$api_base/ping")"
|
||||||
|
if [[ ! "$cors_status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
echo "Stable API rejected the Capacitor iOS CORS preflight with HTTP $cors_status" >&2
|
||||||
|
sed -n '1,20p' "$cors_body" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -Eiq '^access-control-allow-origin:[[:space:]]*capacitor://localhost[[:space:]]*$' "$cors_headers"; then
|
||||||
|
echo "Stable API did not allow the exact capacitor://localhost origin" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -Eiq '^access-control-allow-credentials:[[:space:]]*true[[:space:]]*$' "$cors_headers"; then
|
||||||
|
echo "Stable API did not allow credentialed Capacitor requests" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -Eiq '^access-control-allow-methods:.*[[:space:],]POST([[:space:],]|$)' "$cors_headers"; then
|
||||||
|
echo "Stable API did not allow POST from the Capacitor origin" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -Eiq '^access-control-allow-headers:.*[[:space:],]Authorization([[:space:],]|$)' "$cors_headers"; then
|
||||||
|
echo "Stable API did not allow the Authorization header from the Capacitor origin" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -Eiq '^access-control-allow-headers:.*[[:space:],]Content-Type([[:space:],]|$)' "$cors_headers"; then
|
||||||
|
echo "Stable API did not allow the Content-Type header from the Capacitor origin" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build stable production web payload
|
||||||
|
env:
|
||||||
|
RELEASE_COMMIT_SHA: ${{ env.RESOLVED_SOURCE_SHA }}
|
||||||
|
VITE_API_URL: ${{ env.IOS_DEBUG_API_URL }}
|
||||||
|
VITE_RELEASE_MANAGER_CONTROL_API_URL: ${{ env.IOS_DEBUG_API_URL }}
|
||||||
|
VITE_RELEASE_PUBLIC_GATEWAY_API_URL: https://api-v2.truckwash.io
|
||||||
|
run: |
|
||||||
|
npm run build
|
||||||
|
node -e "const manifest = require('./dist/release-manifest.json'); if (manifest.commit_sha !== process.env.RESOLVED_SOURCE_SHA) { throw new Error('Web release manifest source SHA mismatch'); }"
|
||||||
|
|
||||||
|
- name: Sync and validate iOS shell
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
npx cap sync ios
|
||||||
|
npm run mobile:permissions:check
|
||||||
|
if grep -q 'isa = PBXShellScriptBuildPhase;' "$IOS_PROJECT_PATH/project.pbxproj"; then
|
||||||
|
echo "Unexpected Xcode shell-script build phase detected" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
xcodebuild -resolvePackageDependencies -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME"
|
||||||
|
|
||||||
|
- name: Validate native Debug and Release settings
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
debug_settings="$RUNNER_TEMP/ios-debug-build-settings.txt"
|
||||||
|
release_settings="$RUNNER_TEMP/ios-release-build-settings.txt"
|
||||||
|
xcodebuild -showBuildSettings \
|
||||||
|
-project "$IOS_PROJECT_PATH" \
|
||||||
|
-scheme "$IOS_SCHEME" \
|
||||||
|
-configuration Debug \
|
||||||
|
CODE_SIGNING_ALLOWED=NO > "$debug_settings"
|
||||||
|
xcodebuild -showBuildSettings \
|
||||||
|
-project "$IOS_PROJECT_PATH" \
|
||||||
|
-scheme "$IOS_SCHEME" \
|
||||||
|
-configuration Release \
|
||||||
|
CODE_SIGNING_ALLOWED=NO > "$release_settings"
|
||||||
|
|
||||||
|
grep -Eq '^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = io\.truckwash\.app\.debug$' "$debug_settings"
|
||||||
|
grep -Eq '^[[:space:]]*APP_DISPLAY_NAME = Truck Wash Debug$' "$debug_settings"
|
||||||
|
grep -Eq '^[[:space:]]*PRODUCT_NAME = TruckWashDebug$' "$debug_settings"
|
||||||
|
grep -Eq '^[[:space:]]*CAPACITOR_DEBUG = true$' "$debug_settings"
|
||||||
|
grep -Eq '^[[:space:]]*DEBUG_INFORMATION_FORMAT = dwarf-with-dsym$' "$debug_settings"
|
||||||
|
grep -Eq '^[[:space:]]*IPHONEOS_DEPLOYMENT_TARGET = 15\.0$' "$debug_settings"
|
||||||
|
grep -Eq '^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = io\.truckwash\.app$' "$release_settings"
|
||||||
|
grep -Eq '^[[:space:]]*APP_DISPLAY_NAME = Truck Wash$' "$release_settings"
|
||||||
|
grep -Eq '^[[:space:]]*PRODUCT_NAME = App$' "$release_settings"
|
||||||
|
|
||||||
|
- name: Install and validate Apple development signing assets
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
IOS_DEBUG_CERTIFICATE_BASE64: ${{ secrets.IOS_DEBUG_CERTIFICATE_BASE64 }}
|
||||||
|
IOS_DEBUG_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DEBUG_CERTIFICATE_PASSWORD }}
|
||||||
|
IOS_DEBUG_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_DEBUG_PROVISION_PROFILE_BASE64 }}
|
||||||
|
IOS_DEBUG_ALLOWED_UDIDS: ${{ secrets.IOS_DEBUG_ALLOWED_UDIDS }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
node scripts/mobile/check-ios-debug-signing-env.mjs
|
||||||
|
|
||||||
|
certificate_path="$RUNNER_TEMP/ios-debug-development.p12"
|
||||||
|
profile_path="$RUNNER_TEMP/ios-debug-development.mobileprovision"
|
||||||
|
profile_plist="$RUNNER_TEMP/ios-debug-development-profile.plist"
|
||||||
|
keychain_path="$RUNNER_TEMP/ios-debug-signing.keychain-db"
|
||||||
|
keychain_password="$(openssl rand -base64 48 | tr -d '\n')"
|
||||||
|
echo "::add-mask::$keychain_password"
|
||||||
|
|
||||||
|
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_DEBUG_CERTIFICATE_BASE64.replace(/\\s/g, ''), 'base64'))" "$certificate_path"
|
||||||
|
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_DEBUG_PROVISION_PROFILE_BASE64.replace(/\\s/g, ''), 'base64'))" "$profile_path"
|
||||||
|
chmod 600 "$certificate_path" "$profile_path"
|
||||||
|
security cms -D -i "$profile_path" > "$profile_plist"
|
||||||
|
|
||||||
|
security create-keychain -p "$keychain_password" "$keychain_path"
|
||||||
|
security set-keychain-settings -lut 21600 "$keychain_path"
|
||||||
|
security unlock-keychain -p "$keychain_password" "$keychain_path"
|
||||||
|
security import "$certificate_path" \
|
||||||
|
-P "$IOS_DEBUG_CERTIFICATE_PASSWORD" \
|
||||||
|
-A \
|
||||||
|
-t cert \
|
||||||
|
-f pkcs12 \
|
||||||
|
-k "$keychain_path"
|
||||||
|
security list-keychains -d user -s "$keychain_path" $(security list-keychains -d user | tr -d '"')
|
||||||
|
security set-key-partition-list \
|
||||||
|
-S apple-tool:,apple: \
|
||||||
|
-s \
|
||||||
|
-k "$keychain_password" \
|
||||||
|
"$keychain_path"
|
||||||
|
|
||||||
|
signing_identity_sha="$(security find-identity -v -p codesigning "$keychain_path" | awk '/Apple Development/ {print $2; exit}')"
|
||||||
|
if [[ ! "$signing_identity_sha" =~ ^[0-9A-Fa-f]{40}$ ]]; then
|
||||||
|
echo "The PKCS#12 file does not contain a valid Apple Development signing identity" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
IOS_SIGNING_IDENTITY_SHA="$signing_identity_sha" PROFILE_PLIST="$profile_plist" python3 <<'PY'
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import plistlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with open(os.environ["PROFILE_PLIST"], "rb") as handle:
|
||||||
|
profile = plistlib.load(handle)
|
||||||
|
|
||||||
|
team_id = os.environ["APPLE_TEAM_ID"]
|
||||||
|
bundle_id = os.environ["IOS_DEBUG_BUNDLE_ID"]
|
||||||
|
entitlements = profile.get("Entitlements", {})
|
||||||
|
allowed = {line.strip() for line in os.environ["IOS_DEBUG_ALLOWED_UDIDS"].splitlines() if line.strip()}
|
||||||
|
provisioned = set(profile.get("ProvisionedDevices", []))
|
||||||
|
expiration = profile.get("ExpirationDate")
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
if expiration and expiration.tzinfo is None:
|
||||||
|
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
checks = {
|
||||||
|
"profile team identifier": team_id in profile.get("TeamIdentifier", []),
|
||||||
|
"application identifier": entitlements.get("application-identifier") == f"{team_id}.{bundle_id}",
|
||||||
|
"entitlement team identifier": entitlements.get("com.apple.developer.team-identifier") == team_id,
|
||||||
|
"development entitlement": entitlements.get("get-task-allow") is True,
|
||||||
|
"profile expiration": expiration is not None and expiration > now,
|
||||||
|
"registered devices": bool(allowed) and allowed <= provisioned,
|
||||||
|
"non-enterprise profile": profile.get("ProvisionsAllDevices") is not True,
|
||||||
|
"developer certificate": bool(profile.get("DeveloperCertificates")),
|
||||||
|
"profile UUID": isinstance(profile.get("UUID"), str) and re.fullmatch(r"[0-9A-Fa-f-]{36}", profile["UUID"]) is not None,
|
||||||
|
"safe profile name": isinstance(profile.get("Name"), str) and not any(char in profile["Name"] for char in "\r\n"),
|
||||||
|
}
|
||||||
|
identity_sha = os.environ["IOS_SIGNING_IDENTITY_SHA"].upper()
|
||||||
|
certificate_hashes = {hashlib.sha1(value).hexdigest().upper() for value in profile.get("DeveloperCertificates", [])}
|
||||||
|
checks["certificate belongs to profile"] = identity_sha in certificate_hashes
|
||||||
|
|
||||||
|
failures = [label for label, passed in checks.items() if not passed]
|
||||||
|
if failures:
|
||||||
|
print("Development provisioning profile validation failed:", file=sys.stderr)
|
||||||
|
for failure in failures:
|
||||||
|
print(f"- {failure}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
|
||||||
|
profile_uuid="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$profile_plist")"
|
||||||
|
profile_name="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$profile_plist")"
|
||||||
|
profile_expiration="$(PROFILE_PLIST="$profile_plist" python3 - <<'PY'
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import plistlib
|
||||||
|
|
||||||
|
with open(os.environ["PROFILE_PLIST"], "rb") as handle:
|
||||||
|
expiration = plistlib.load(handle)["ExpirationDate"]
|
||||||
|
if expiration.tzinfo is None:
|
||||||
|
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
|
||||||
|
print(expiration.astimezone(datetime.timezone.utc).isoformat().replace("+00:00", "Z"))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
profile_install_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||||
|
profile_install_path="$profile_install_dir/$profile_uuid.mobileprovision"
|
||||||
|
mkdir -p "$profile_install_dir"
|
||||||
|
cp "$profile_path" "$profile_install_path"
|
||||||
|
|
||||||
|
echo "IOS_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
|
||||||
|
echo "IOS_PROFILE_INSTALL_PATH=$profile_install_path" >> "$GITHUB_ENV"
|
||||||
|
echo "IOS_PROFILE_NAME=$profile_name" >> "$GITHUB_ENV"
|
||||||
|
echo "IOS_PROFILE_UUID=$profile_uuid" >> "$GITHUB_ENV"
|
||||||
|
echo "IOS_PROFILE_EXPIRATION=$profile_expiration" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Archive Debug app with Apple Development signing
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
archive_path="$RUNNER_TEMP/TruckWashDebug.xcarchive"
|
||||||
|
xcodebuild \
|
||||||
|
-project "$IOS_PROJECT_PATH" \
|
||||||
|
-scheme "$IOS_SCHEME" \
|
||||||
|
-configuration Debug \
|
||||||
|
-destination "generic/platform=iOS" \
|
||||||
|
-archivePath "$archive_path" \
|
||||||
|
archive \
|
||||||
|
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
|
||||||
|
CODE_SIGN_STYLE=Manual \
|
||||||
|
CODE_SIGN_IDENTITY="Apple Development" \
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER="$IOS_PROFILE_NAME" \
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER="$IOS_DEBUG_BUNDLE_ID" \
|
||||||
|
MARKETING_VERSION="$MOBILE_VERSION_NAME" \
|
||||||
|
CURRENT_PROJECT_VERSION="$MOBILE_VERSION_CODE" \
|
||||||
|
DEBUG_INFORMATION_FORMAT="dwarf-with-dsym" \
|
||||||
|
ONLY_ACTIVE_ARCH=NO
|
||||||
|
echo "IOS_ARCHIVE_PATH=$archive_path" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Export development IPA
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
export_options="$RUNNER_TEMP/ios-debug-ExportOptions.plist"
|
||||||
|
export_path="$RUNNER_TEMP/ios-debug-export"
|
||||||
|
EXPORT_OPTIONS="$export_options" python3 <<'PY'
|
||||||
|
import os
|
||||||
|
import plistlib
|
||||||
|
|
||||||
|
options = {
|
||||||
|
"method": "development",
|
||||||
|
"signingStyle": "manual",
|
||||||
|
"teamID": os.environ["APPLE_TEAM_ID"],
|
||||||
|
"provisioningProfiles": {
|
||||||
|
os.environ["IOS_DEBUG_BUNDLE_ID"]: os.environ["IOS_PROFILE_NAME"],
|
||||||
|
},
|
||||||
|
"stripSwiftSymbols": True,
|
||||||
|
"manageAppVersionAndBuildNumber": False,
|
||||||
|
}
|
||||||
|
with open(os.environ["EXPORT_OPTIONS"], "wb") as handle:
|
||||||
|
plistlib.dump(options, handle)
|
||||||
|
PY
|
||||||
|
xcodebuild \
|
||||||
|
-exportArchive \
|
||||||
|
-archivePath "$IOS_ARCHIVE_PATH" \
|
||||||
|
-exportPath "$export_path" \
|
||||||
|
-exportOptionsPlist "$export_options"
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
ipa_files=("$export_path"/*.ipa)
|
||||||
|
if [[ ${#ipa_files[@]} -ne 1 ]]; then
|
||||||
|
echo "Expected exactly one exported IPA; found ${#ipa_files[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "IOS_EXPORTED_IPA=${ipa_files[0]}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Validate exported IPA and embedded signature
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
IOS_DEBUG_ALLOWED_UDIDS: ${{ secrets.IOS_DEBUG_ALLOWED_UDIDS }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
inspect_dir="$RUNNER_TEMP/ios-debug-inspect"
|
||||||
|
mkdir -p "$inspect_dir"
|
||||||
|
unzip -q "$IOS_EXPORTED_IPA" -d "$inspect_dir"
|
||||||
|
shopt -s nullglob
|
||||||
|
app_bundles=("$inspect_dir"/Payload/*.app)
|
||||||
|
if [[ ${#app_bundles[@]} -ne 1 ]]; then
|
||||||
|
echo "Expected exactly one Payload app; found ${#app_bundles[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
app_path="${app_bundles[0]}"
|
||||||
|
app_info="$app_path/Info.plist"
|
||||||
|
embedded_profile="$RUNNER_TEMP/ios-debug-embedded-profile.plist"
|
||||||
|
signature_entitlements="$RUNNER_TEMP/ios-debug-signature-entitlements.plist"
|
||||||
|
signature_details="$RUNNER_TEMP/ios-debug-signature-details.txt"
|
||||||
|
security cms -D -i "$app_path/embedded.mobileprovision" > "$embedded_profile"
|
||||||
|
codesign --verify --deep --strict "$app_path"
|
||||||
|
codesign -d --entitlements :- "$app_path" > "$signature_entitlements"
|
||||||
|
codesign -dvv "$app_path" > /dev/null 2> "$signature_details"
|
||||||
|
grep -Fq "TeamIdentifier=$APPLE_TEAM_ID" "$signature_details"
|
||||||
|
grep -Eq '^Authority=Apple Development:' "$signature_details"
|
||||||
|
|
||||||
|
executable_name="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app_info")"
|
||||||
|
architectures="$(lipo -archs "$app_path/$executable_name")"
|
||||||
|
if [[ " $architectures " != *" arm64 "* ]]; then
|
||||||
|
echo "Exported executable does not contain arm64" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
APP_INFO="$app_info" PROFILE_PLIST="$embedded_profile" SIGNATURE_ENTITLEMENTS="$signature_entitlements" python3 <<'PY'
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import plistlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def load(path):
|
||||||
|
with open(path, "rb") as handle:
|
||||||
|
return plistlib.load(handle)
|
||||||
|
|
||||||
|
info = load(os.environ["APP_INFO"])
|
||||||
|
profile = load(os.environ["PROFILE_PLIST"])
|
||||||
|
signature = load(os.environ["SIGNATURE_ENTITLEMENTS"])
|
||||||
|
profile_entitlements = profile.get("Entitlements", {})
|
||||||
|
team_id = os.environ["APPLE_TEAM_ID"]
|
||||||
|
bundle_id = os.environ["IOS_DEBUG_BUNDLE_ID"]
|
||||||
|
allowed = {line.strip() for line in os.environ["IOS_DEBUG_ALLOWED_UDIDS"].splitlines() if line.strip()}
|
||||||
|
provisioned = set(profile.get("ProvisionedDevices", []))
|
||||||
|
expiration = profile.get("ExpirationDate")
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
if expiration and expiration.tzinfo is None:
|
||||||
|
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
checks = {
|
||||||
|
"bundle identifier": info.get("CFBundleIdentifier") == bundle_id,
|
||||||
|
"display name": info.get("CFBundleDisplayName") == "Truck Wash Debug",
|
||||||
|
"debug executable": info.get("CFBundleExecutable") == "TruckWashDebug",
|
||||||
|
"marketing version": info.get("CFBundleShortVersionString") == os.environ["MOBILE_VERSION_NAME"],
|
||||||
|
"build number": info.get("CFBundleVersion") == os.environ["MOBILE_VERSION_CODE"],
|
||||||
|
"minimum iOS": info.get("MinimumOSVersion") == "15.0",
|
||||||
|
"profile UUID": profile.get("UUID") == os.environ["IOS_PROFILE_UUID"],
|
||||||
|
"profile team": team_id in profile.get("TeamIdentifier", []),
|
||||||
|
"profile application identifier": profile_entitlements.get("application-identifier") == f"{team_id}.{bundle_id}",
|
||||||
|
"development profile": profile_entitlements.get("get-task-allow") is True,
|
||||||
|
"signature application identifier": signature.get("application-identifier") == f"{team_id}.{bundle_id}",
|
||||||
|
"signature team identifier": signature.get("com.apple.developer.team-identifier") == team_id,
|
||||||
|
"debuggable signature": signature.get("get-task-allow") is True,
|
||||||
|
"profile expiration": expiration is not None and expiration > now,
|
||||||
|
"registered devices": bool(allowed) and allowed <= provisioned,
|
||||||
|
"non-enterprise profile": profile.get("ProvisionsAllDevices") is not True,
|
||||||
|
}
|
||||||
|
failures = [label for label, passed in checks.items() if not passed]
|
||||||
|
if failures:
|
||||||
|
print("Exported development IPA validation failed:", file=sys.stderr)
|
||||||
|
for failure in failures:
|
||||||
|
print(f"- {failure}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: Assemble debug artifact
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
SOURCE_REF: ${{ inputs.source_ref }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
short_sha="${RESOLVED_SOURCE_SHA:0:12}"
|
||||||
|
artifact_name="truck-wash-debug-${MOBILE_VERSION_NAME}-${short_sha}"
|
||||||
|
artifact_dir="$RUNNER_TEMP/device-debug-artifact"
|
||||||
|
ipa_filename="$artifact_name.ipa"
|
||||||
|
dsym_filename="$artifact_name.dSYM.zip"
|
||||||
|
mkdir -p "$artifact_dir"
|
||||||
|
cp "$IOS_EXPORTED_IPA" "$artifact_dir/$ipa_filename"
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
dsym_bundles=("$IOS_ARCHIVE_PATH"/dSYMs/*.dSYM)
|
||||||
|
if [[ ${#dsym_bundles[@]} -eq 0 ]]; then
|
||||||
|
echo "The Debug archive did not contain any dSYM bundles" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ditto -c -k --sequesterRsrc --keepParent "$IOS_ARCHIVE_PATH/dSYMs" "$artifact_dir/$dsym_filename"
|
||||||
|
|
||||||
|
capacitor_version="$(node -p "require('./node_modules/@capacitor/core/package.json').version")"
|
||||||
|
BUILT_AT_UTC="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
|
||||||
|
CAPACITOR_VERSION="$capacitor_version" \
|
||||||
|
DSYM_FILENAME="$dsym_filename" \
|
||||||
|
IPA_FILENAME="$ipa_filename" \
|
||||||
|
MANIFEST_PATH="$artifact_dir/manifest.json" \
|
||||||
|
python3 <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"repository": os.environ["GITHUB_REPOSITORY"],
|
||||||
|
"source_ref": os.environ["SOURCE_REF"],
|
||||||
|
"source_sha": os.environ["RESOLVED_SOURCE_SHA"],
|
||||||
|
"workflow_run": int(os.environ["GITHUB_RUN_NUMBER"]),
|
||||||
|
"workflow_attempt": int(os.environ["GITHUB_RUN_ATTEMPT"]),
|
||||||
|
"built_at_utc": os.environ["BUILT_AT_UTC"],
|
||||||
|
"api_url": os.environ["IOS_DEBUG_API_URL"],
|
||||||
|
"release_manager_control_api_url": os.environ["IOS_DEBUG_API_URL"],
|
||||||
|
"bundle_id": os.environ["IOS_DEBUG_BUNDLE_ID"],
|
||||||
|
"display_name": "Truck Wash Debug",
|
||||||
|
"executable_name": "TruckWashDebug",
|
||||||
|
"version": os.environ["MOBILE_VERSION_NAME"],
|
||||||
|
"build": os.environ["MOBILE_VERSION_CODE"],
|
||||||
|
"minimum_ios": "15.0",
|
||||||
|
"capacitor_version": os.environ["CAPACITOR_VERSION"],
|
||||||
|
"xcode_version": os.environ["XCODE_VERSION"],
|
||||||
|
"signing_method": "development",
|
||||||
|
"profile_expiration_utc": os.environ["IOS_PROFILE_EXPIRATION"],
|
||||||
|
"ipa_filename": os.environ["IPA_FILENAME"],
|
||||||
|
"dsym_filename": os.environ["DSYM_FILENAME"],
|
||||||
|
}
|
||||||
|
with open(os.environ["MANIFEST_PATH"], "w", encoding="utf-8") as handle:
|
||||||
|
json.dump(manifest, handle, indent=2, sort_keys=True)
|
||||||
|
handle.write("\n")
|
||||||
|
PY
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$artifact_dir"
|
||||||
|
shasum -a 256 "$ipa_filename" "$dsym_filename" manifest.json > SHA256SUMS
|
||||||
|
)
|
||||||
|
echo "IOS_DEBUG_ARTIFACT_DIR=$artifact_dir" >> "$GITHUB_ENV"
|
||||||
|
echo "IOS_DEBUG_ARTIFACT_NAME=$artifact_name" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Upload device-debug artifact
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: ${{ env.IOS_DEBUG_ARTIFACT_NAME }}
|
||||||
|
path: ${{ env.IOS_DEBUG_ARTIFACT_DIR }}
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Clean up Apple signing assets
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ -n "${IOS_KEYCHAIN_PATH:-}" ]]; then
|
||||||
|
security delete-keychain "$IOS_KEYCHAIN_PATH" || true
|
||||||
|
else
|
||||||
|
security delete-keychain "$RUNNER_TEMP/ios-debug-signing.keychain-db" || true
|
||||||
|
fi
|
||||||
|
if [[ -n "${IOS_PROFILE_INSTALL_PATH:-}" ]]; then
|
||||||
|
rm -f "$IOS_PROFILE_INSTALL_PATH"
|
||||||
|
fi
|
||||||
|
rm -f \
|
||||||
|
"$RUNNER_TEMP/ios-debug-development.p12" \
|
||||||
|
"$RUNNER_TEMP/ios-debug-development.mobileprovision" \
|
||||||
|
"$RUNNER_TEMP/ios-debug-development-profile.plist" \
|
||||||
|
"$RUNNER_TEMP/ios-debug-embedded-profile.plist" \
|
||||||
|
"$RUNNER_TEMP/ios-debug-signature-entitlements.plist" \
|
||||||
|
"$RUNNER_TEMP/ios-debug-signature-details.txt"
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
name: iOS Internal TestFlight
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [Frontend Release]
|
||||||
|
types: [completed]
|
||||||
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
source_sha:
|
||||||
|
description: Full master commit SHA with a verified Frontend Release proof
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
confirmation:
|
||||||
|
description: Type UPLOAD IOS INTERNAL BUILD
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ios-internal-testflight
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
prepare:
|
||||||
|
name: Resolve verified release
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.event.workflow_run.event == 'workflow_run' &&
|
||||||
|
github.event.workflow_run.head_branch == 'master' &&
|
||||||
|
github.event.workflow_run.head_repository.full_name == github.repository)
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 10
|
||||||
|
outputs:
|
||||||
|
source_sha: ${{ steps.resolve.outputs.source_sha }}
|
||||||
|
enabled: ${{ steps.resolve.outputs.enabled }}
|
||||||
|
current: ${{ steps.resolve.outputs.current }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository history
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Resolve immutable source and rollout gate
|
||||||
|
id: resolve
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
|
WORKFLOW_SOURCE_SHA: ${{ github.event.workflow_run.head_sha || '' }}
|
||||||
|
INPUT_SOURCE_SHA: ${{ inputs.source_sha || '' }}
|
||||||
|
CONFIRMATION: ${{ inputs.confirmation || '' }}
|
||||||
|
AUTOMATION_ENABLED: ${{ vars.APP_STORE_AUTOMATION_ENABLED || 'false' }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
source_sha="$WORKFLOW_SOURCE_SHA"
|
||||||
|
if [[ "$EVENT_NAME" == workflow_dispatch ]]; then
|
||||||
|
[[ "$GITHUB_REF" == refs/heads/master ]] || { echo "Dispatch this workflow from master." >&2; exit 1; }
|
||||||
|
[[ "$CONFIRMATION" == "UPLOAD IOS INTERNAL BUILD" ]] || { echo "Invalid confirmation." >&2; exit 1; }
|
||||||
|
source_sha="${INPUT_SOURCE_SHA,,}"
|
||||||
|
fi
|
||||||
|
[[ "$source_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "A full lowercase source SHA is required." >&2; exit 1; }
|
||||||
|
git show-ref --verify --quiet refs/remotes/origin/master || { echo "origin/master was not included in the full checkout." >&2; exit 1; }
|
||||||
|
git cat-file -e "${source_sha}^{commit}"
|
||||||
|
git merge-base --is-ancestor "$source_sha" origin/master || { echo "Source is not reachable from master." >&2; exit 1; }
|
||||||
|
current=false
|
||||||
|
[[ "$(git rev-parse origin/master)" == "$source_sha" ]] && current=true
|
||||||
|
enabled=false
|
||||||
|
[[ "$AUTOMATION_ENABLED" == true ]] && enabled=true
|
||||||
|
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "current=$current" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
|
||||||
|
if [[ "$enabled" != true ]]; then
|
||||||
|
echo "### iOS automation is safely disabled" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo 'Set repository variable `APP_STORE_AUTOMATION_ENABLED=true` only after the signing/API credential canary passes.' >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
elif [[ "$current" != true ]]; then
|
||||||
|
echo "### Stale release skipped" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "The verified SHA is no longer current master." >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Require green WebKit mobile tests before App Store upload
|
||||||
|
if: steps.resolve.outputs.enabled == 'true' && steps.resolve.outputs.current == 'true'
|
||||||
|
run: node scripts/mobile/verify-store-test-gate.mjs --platform apple
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
STORE_SOURCE_SHA: ${{ steps.resolve.outputs.source_sha }}
|
||||||
|
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||||
|
|
||||||
|
deliver:
|
||||||
|
name: Sign, upload, process, and distribute
|
||||||
|
needs: prepare
|
||||||
|
if: needs.prepare.outputs.enabled == 'true' && needs.prepare.outputs.current == 'true'
|
||||||
|
runs-on: macos-15
|
||||||
|
timeout-minutes: 120
|
||||||
|
environment: app-store-signing
|
||||||
|
env:
|
||||||
|
DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer
|
||||||
|
IOS_SOURCE_SHA: ${{ needs.prepare.outputs.source_sha }}
|
||||||
|
IOS_PROJECT_PATH: ${{ vars.IOS_PROJECT || 'ios/App/App.xcodeproj' }}
|
||||||
|
IOS_SCHEME: ${{ vars.IOS_SCHEME || 'App' }}
|
||||||
|
IOS_BUNDLE_ID: ${{ vars.IOS_BUNDLE_ID || 'io.truckwash.app' }}
|
||||||
|
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||||
|
APP_STORE_CONNECT_API_KEY_ID: ${{ vars.APP_STORE_CONNECT_API_KEY_ID }}
|
||||||
|
APP_STORE_CONNECT_ISSUER_ID: ${{ vars.APP_STORE_CONNECT_ISSUER_ID || '' }}
|
||||||
|
APP_STORE_CONNECT_APP_ID: ${{ vars.APP_STORE_CONNECT_APP_ID }}
|
||||||
|
TESTFLIGHT_INTERNAL_GROUP_ID: ${{ vars.TESTFLIGHT_INTERNAL_GROUP_ID }}
|
||||||
|
APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout verified source
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
ref: ${{ env.IOS_SOURCE_SHA }}
|
||||||
|
fetch-depth: 1
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Download and verify frontend release proof
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
TRIGGERING_RELEASE_RUN_ID: ${{ github.event.workflow_run.id || '' }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
artifact_name="frontend-release-proof-$IOS_SOURCE_SHA"
|
||||||
|
response="$RUNNER_TEMP/proof-artifacts.json"
|
||||||
|
curl --fail --silent --show-error --location \
|
||||||
|
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||||
|
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts?name=$artifact_name&per_page=100" > "$response"
|
||||||
|
artifact_id="$(jq -r --arg run "$TRIGGERING_RELEASE_RUN_ID" '
|
||||||
|
[.artifacts[] | select(.expired == false) | select(($run == "") or ((.workflow_run.id|tostring) == $run))] |
|
||||||
|
sort_by(.created_at) | last | .id // empty' "$response")"
|
||||||
|
[[ "$artifact_id" =~ ^[0-9]+$ ]] || { echo "No verified Frontend Release proof found for $IOS_SOURCE_SHA." >&2; exit 1; }
|
||||||
|
mkdir -p output/frontend-release-proof
|
||||||
|
curl --fail --silent --show-error --location \
|
||||||
|
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||||
|
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id/zip" -o "$RUNNER_TEMP/proof.zip"
|
||||||
|
unzip -q "$RUNNER_TEMP/proof.zip" -d output/frontend-release-proof
|
||||||
|
PROOF_PATH=output/frontend-release-proof/frontend-release-proof.json node <<'NODE'
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const proof = JSON.parse(fs.readFileSync(process.env.PROOF_PATH, "utf8"));
|
||||||
|
const checks = {
|
||||||
|
schema: proof.schemaVersion === 2,
|
||||||
|
repository: proof.repository === process.env.GITHUB_REPOSITORY,
|
||||||
|
source: proof.sourceSha === process.env.IOS_SOURCE_SHA,
|
||||||
|
exactSource: proof.sha === process.env.IOS_SOURCE_SHA,
|
||||||
|
releaseIdentity: typeof proof.releaseId === "string" && proof.releaseId.length > 0,
|
||||||
|
archive: /^[a-f0-9]{64}$/.test(proof.archiveSha256 || ""),
|
||||||
|
activeTarget: typeof proof.activeTarget === "string" && proof.activeTarget.length > 0,
|
||||||
|
verification: proof.verificationState === "verified",
|
||||||
|
publicGate: proof.livePublicGate === "passed",
|
||||||
|
credentialedGate: ["passed", "not-configured"].includes(proof.liveCredentialedGate),
|
||||||
|
managerGate: proof.releaseManagerGate === "passed",
|
||||||
|
serverVersion: proof.serverVersionUpdated === true,
|
||||||
|
serverVersionReadBack: proof.serverVersionReadBack === "passed",
|
||||||
|
};
|
||||||
|
const failures = Object.entries(checks).filter(([, passed]) => !passed).map(([label]) => label);
|
||||||
|
if (failures.length) throw new Error(`Invalid frontend release proof: ${failures.join(", ")}`);
|
||||||
|
NODE
|
||||||
|
|
||||||
|
- name: Verify Xcode 26 and iOS 26 SDK
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
[[ -x "$DEVELOPER_DIR/usr/bin/xcodebuild" ]] || { echo "Xcode 26.3 is not installed at $DEVELOPER_DIR." >&2; exit 1; }
|
||||||
|
xcode_version="$(xcodebuild -version | sed -n '1p')"
|
||||||
|
sdk_version="$(xcrun --sdk iphoneos --show-sdk-version)"
|
||||||
|
[[ "$xcode_version" =~ ^Xcode\ 26\. ]] || { echo "Xcode 26.x required; found $xcode_version." >&2; exit 1; }
|
||||||
|
[[ "$sdk_version" =~ ^26\. ]] || { echo "iPhoneOS 26 SDK required; found $sdk_version." >&2; exit 1; }
|
||||||
|
echo "XCODE_VERSION=$xcode_version" >> "$GITHUB_ENV"
|
||||||
|
echo "IOS_SDK_VERSION=$sdk_version" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Setup Ruby and pinned Fastlane
|
||||||
|
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1
|
||||||
|
with:
|
||||||
|
ruby-version: "3.3"
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Install web dependencies
|
||||||
|
run: npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
- name: Validate storefront and resolve version
|
||||||
|
id: version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
node scripts/mobile/validate-app-store.mjs
|
||||||
|
version="$(node -p "JSON.parse(require('fs').readFileSync('ios/release.json')).marketingVersion")"
|
||||||
|
bundle="$(node -p "JSON.parse(require('fs').readFileSync('ios/release.json')).bundleId")"
|
||||||
|
[[ "$bundle" == "$IOS_BUNDLE_ID" ]]
|
||||||
|
echo "IOS_MARKETING_VERSION=$version" >> "$GITHUB_ENV"
|
||||||
|
echo "MOBILE_VERSION_NAME=$version" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Resolve build number from App Store Connect
|
||||||
|
id: app-store
|
||||||
|
run: node scripts/mobile/app-store-connect.mjs next-build-number
|
||||||
|
|
||||||
|
- name: Export resolved build number
|
||||||
|
env:
|
||||||
|
BUILD_NUMBER: ${{ steps.app-store.outputs.build_number }}
|
||||||
|
run: |
|
||||||
|
[[ "$BUILD_NUMBER" =~ ^[1-9][0-9]*$ ]]
|
||||||
|
echo "IOS_BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_ENV"
|
||||||
|
echo "MOBILE_VERSION_CODE=$BUILD_NUMBER" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Validate complete Apple environment
|
||||||
|
env:
|
||||||
|
IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64 }}
|
||||||
|
IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
|
||||||
|
IOS_APP_STORE_PROFILE_BASE64: ${{ secrets.IOS_APP_STORE_PROFILE_BASE64 }}
|
||||||
|
UPLOAD_IOS_TO_APP_STORE: "true"
|
||||||
|
run: node scripts/mobile/check-store-upload-env.mjs --ios
|
||||||
|
|
||||||
|
- name: Build and sync production iOS shell
|
||||||
|
run: |
|
||||||
|
npm run build
|
||||||
|
npx cap sync ios
|
||||||
|
npm run mobile:permissions:check
|
||||||
|
xcodebuild -resolvePackageDependencies -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME"
|
||||||
|
|
||||||
|
- name: Validate native release settings
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
settings="$RUNNER_TEMP/ios-release-build-settings.txt"
|
||||||
|
xcodebuild -showBuildSettings -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME" -configuration Release CODE_SIGNING_ALLOWED=NO > "$settings"
|
||||||
|
grep -Eq "^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = ${IOS_BUNDLE_ID//./\.}$" "$settings"
|
||||||
|
grep -Eq '^[[:space:]]*APP_DISPLAY_NAME = Truck Wash$' "$settings"
|
||||||
|
grep -Eq '^[[:space:]]*IPHONEOS_DEPLOYMENT_TARGET = 15\.0$' "$settings"
|
||||||
|
if grep -q 'isa = PBXShellScriptBuildPhase;' "$IOS_PROJECT_PATH/project.pbxproj"; then
|
||||||
|
echo "Unexpected Xcode shell-script build phase detected." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install and validate Apple distribution signing assets
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64 }}
|
||||||
|
IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
|
||||||
|
IOS_APP_STORE_PROFILE_BASE64: ${{ secrets.IOS_APP_STORE_PROFILE_BASE64 }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
certificate_path="$RUNNER_TEMP/apple-distribution.p12"
|
||||||
|
profile_path="$RUNNER_TEMP/app-store.mobileprovision"
|
||||||
|
profile_plist="$RUNNER_TEMP/app-store-profile.plist"
|
||||||
|
keychain_path="$RUNNER_TEMP/app-store-signing.keychain-db"
|
||||||
|
keychain_password="$(openssl rand -base64 48 | tr -d '\n')"
|
||||||
|
echo "::add-mask::$keychain_password"
|
||||||
|
echo "IOS_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
|
||||||
|
node -e "const fs=require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64.replace(/\\s/g,''),'base64'))" "$certificate_path"
|
||||||
|
node -e "const fs=require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_APP_STORE_PROFILE_BASE64.replace(/\\s/g,''),'base64'))" "$profile_path"
|
||||||
|
chmod 600 "$certificate_path" "$profile_path"
|
||||||
|
security cms -D -i "$profile_path" > "$profile_plist"
|
||||||
|
security create-keychain -p "$keychain_password" "$keychain_path"
|
||||||
|
security set-keychain-settings -lut 21600 "$keychain_path"
|
||||||
|
security unlock-keychain -p "$keychain_password" "$keychain_path"
|
||||||
|
security import "$certificate_path" -P "$IOS_DISTRIBUTION_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain_path"
|
||||||
|
security list-keychains -d user -s "$keychain_path" $(security list-keychains -d user | tr -d '"')
|
||||||
|
security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain_path"
|
||||||
|
identity_sha="$(security find-identity -v -p codesigning "$keychain_path" | awk '/Apple Distribution/ {print $2; exit}')"
|
||||||
|
[[ "$identity_sha" =~ ^[0-9A-Fa-f]{40}$ ]] || { echo "P12 lacks an Apple Distribution identity." >&2; exit 1; }
|
||||||
|
IOS_SIGNING_IDENTITY_SHA="$identity_sha" PROFILE_PLIST="$profile_plist" python3 <<'PY'
|
||||||
|
import datetime, hashlib, os, plistlib, re, sys
|
||||||
|
with open(os.environ["PROFILE_PLIST"], "rb") as handle: profile = plistlib.load(handle)
|
||||||
|
entitlements = profile.get("Entitlements", {})
|
||||||
|
expiration = profile.get("ExpirationDate")
|
||||||
|
if expiration and expiration.tzinfo is None: expiration = expiration.replace(tzinfo=datetime.timezone.utc)
|
||||||
|
team = os.environ["APPLE_TEAM_ID"]
|
||||||
|
bundle = os.environ["IOS_BUNDLE_ID"]
|
||||||
|
hashes = {hashlib.sha1(value).hexdigest().upper() for value in profile.get("DeveloperCertificates", [])}
|
||||||
|
checks = {
|
||||||
|
"team": team in profile.get("TeamIdentifier", []),
|
||||||
|
"application identifier": entitlements.get("application-identifier") == f"{team}.{bundle}",
|
||||||
|
"team entitlement": entitlements.get("com.apple.developer.team-identifier") == team,
|
||||||
|
"distribution entitlement": entitlements.get("get-task-allow") is False,
|
||||||
|
"App Store profile has no devices": not profile.get("ProvisionedDevices"),
|
||||||
|
"non-enterprise profile": profile.get("ProvisionsAllDevices") is not True,
|
||||||
|
"expiration": expiration is not None and expiration > datetime.datetime.now(datetime.timezone.utc),
|
||||||
|
"certificate belongs to profile": os.environ["IOS_SIGNING_IDENTITY_SHA"].upper() in hashes,
|
||||||
|
"safe profile name": isinstance(profile.get("Name"), str) and not re.search(r"[\r\n]", profile["Name"]),
|
||||||
|
}
|
||||||
|
failed = [name for name, passed in checks.items() if not passed]
|
||||||
|
if failed:
|
||||||
|
print("Distribution signing validation failed:", *[f"- {name}" for name in failed], sep="\n", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
profile_uuid="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$profile_plist")"
|
||||||
|
profile_name="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$profile_plist")"
|
||||||
|
profile_install="$HOME/Library/MobileDevice/Provisioning Profiles/$profile_uuid.mobileprovision"
|
||||||
|
mkdir -p "$(dirname "$profile_install")"
|
||||||
|
echo "IOS_PROFILE_INSTALL_PATH=$profile_install" >> "$GITHUB_ENV"
|
||||||
|
cp "$profile_path" "$profile_install"
|
||||||
|
echo "IOS_PROFILE_NAME=$profile_name" >> "$GITHUB_ENV"
|
||||||
|
echo "IOS_PROFILE_UUID=$profile_uuid" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Archive and export App Store IPA
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
archive="$RUNNER_TEMP/TruckWash.xcarchive"
|
||||||
|
export_dir="$RUNNER_TEMP/ios-export"
|
||||||
|
export_options="$RUNNER_TEMP/ExportOptions.plist"
|
||||||
|
xcodebuild -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME" -configuration Release \
|
||||||
|
-destination 'generic/platform=iOS' -archivePath "$archive" archive \
|
||||||
|
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY='Apple Distribution' \
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER="$IOS_PROFILE_NAME" MARKETING_VERSION="$IOS_MARKETING_VERSION" \
|
||||||
|
CURRENT_PROJECT_VERSION="$IOS_BUILD_NUMBER" DEBUG_INFORMATION_FORMAT='dwarf-with-dsym'
|
||||||
|
EXPORT_OPTIONS="$export_options" python3 <<'PY'
|
||||||
|
import os, plistlib
|
||||||
|
options = {"method":"app-store-connect","signingStyle":"manual","teamID":os.environ["APPLE_TEAM_ID"],"provisioningProfiles":{os.environ["IOS_BUNDLE_ID"]:os.environ["IOS_PROFILE_NAME"]},"stripSwiftSymbols":True,"manageAppVersionAndBuildNumber":False}
|
||||||
|
with open(os.environ["EXPORT_OPTIONS"], "wb") as handle: plistlib.dump(options, handle)
|
||||||
|
PY
|
||||||
|
xcodebuild -exportArchive -archivePath "$archive" -exportPath "$export_dir" -exportOptionsPlist "$export_options"
|
||||||
|
shopt -s nullglob
|
||||||
|
ipa_files=("$export_dir"/*.ipa)
|
||||||
|
[[ ${#ipa_files[@]} -eq 1 ]] || { echo "Expected one IPA; found ${#ipa_files[@]}." >&2; exit 1; }
|
||||||
|
echo "IOS_ARCHIVE_PATH=$archive" >> "$GITHUB_ENV"
|
||||||
|
echo "IOS_IPA_PATH=${ipa_files[0]}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Inspect signed IPA
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
inspect="$RUNNER_TEMP/ios-inspect"
|
||||||
|
unzip -q "$IOS_IPA_PATH" -d "$inspect"
|
||||||
|
shopt -s nullglob
|
||||||
|
apps=("$inspect"/Payload/*.app)
|
||||||
|
[[ ${#apps[@]} -eq 1 ]] || { echo "Expected one Payload app." >&2; exit 1; }
|
||||||
|
app="${apps[0]}"
|
||||||
|
codesign --verify --deep --strict "$app"
|
||||||
|
codesign -d --entitlements :- "$app" > "$RUNNER_TEMP/entitlements.plist"
|
||||||
|
security cms -D -i "$app/embedded.mobileprovision" > "$RUNNER_TEMP/embedded-profile.plist"
|
||||||
|
[[ -f "$app/PrivacyInfo.xcprivacy" ]]
|
||||||
|
[[ -f "$app/da.lproj/InfoPlist.strings" ]]
|
||||||
|
[[ -f "$app/en.lproj/InfoPlist.strings" ]]
|
||||||
|
APP_PATH="$app" python3 <<'PY'
|
||||||
|
import os, plistlib, sys
|
||||||
|
app = os.environ["APP_PATH"]
|
||||||
|
with open(f"{app}/Info.plist", "rb") as handle: info = plistlib.load(handle)
|
||||||
|
with open(os.path.join(os.environ["RUNNER_TEMP"], "entitlements.plist"), "rb") as handle: ent = plistlib.load(handle)
|
||||||
|
with open(os.path.join(os.environ["RUNNER_TEMP"], "embedded-profile.plist"), "rb") as handle: profile = plistlib.load(handle)
|
||||||
|
checks = {
|
||||||
|
"bundle": info.get("CFBundleIdentifier") == os.environ["IOS_BUNDLE_ID"],
|
||||||
|
"version": info.get("CFBundleShortVersionString") == os.environ["IOS_MARKETING_VERSION"],
|
||||||
|
"build": info.get("CFBundleVersion") == os.environ["IOS_BUILD_NUMBER"],
|
||||||
|
"minimum iOS": info.get("MinimumOSVersion") == "15.0",
|
||||||
|
"profile": profile.get("UUID") == os.environ["IOS_PROFILE_UUID"],
|
||||||
|
"non-debug signature": ent.get("get-task-allow") is not True,
|
||||||
|
"signature application id": ent.get("application-identifier") == f'{os.environ["APPLE_TEAM_ID"]}.{os.environ["IOS_BUNDLE_ID"]}',
|
||||||
|
}
|
||||||
|
failed = [name for name, passed in checks.items() if not passed]
|
||||||
|
if failed:
|
||||||
|
print("IPA validation failed:", *[f"- {name}" for name in failed], sep="\n", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: Recheck live master before upload
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
live_master_sha="$(curl --fail --silent --show-error --location \
|
||||||
|
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||||
|
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/master" | jq -r '.object.sha')"
|
||||||
|
[[ "$live_master_sha" == "$IOS_SOURCE_SHA" ]] || {
|
||||||
|
echo "master advanced while the signed build was queued; refusing upload." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
- name: Upload and wait for App Store processing
|
||||||
|
env:
|
||||||
|
TESTFLIGHT_WHAT_TO_TEST: Automatisk intern build fra verificeret master ${{ env.IOS_SOURCE_SHA }}.
|
||||||
|
run: bundle exec fastlane ios upload_internal
|
||||||
|
|
||||||
|
- name: Assign exact processed build to Internal QA
|
||||||
|
id: distribute
|
||||||
|
env:
|
||||||
|
TESTFLIGHT_WHAT_TO_TEST: Automatisk intern build fra verificeret master ${{ env.IOS_SOURCE_SHA }}.
|
||||||
|
run: node scripts/mobile/app-store-connect.mjs wait-and-distribute
|
||||||
|
|
||||||
|
- name: Assemble signed release evidence
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
APP_STORE_BUILD_ID: ${{ steps.distribute.outputs.app_store_build_id }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
artifact="output/ios-release"
|
||||||
|
mkdir -p "$artifact"
|
||||||
|
cp "$IOS_IPA_PATH" "$artifact/TruckWash-$IOS_MARKETING_VERSION-$IOS_BUILD_NUMBER.ipa"
|
||||||
|
shopt -s nullglob
|
||||||
|
dsyms=("$IOS_ARCHIVE_PATH"/dSYMs/*.dSYM)
|
||||||
|
[[ ${#dsyms[@]} -gt 0 ]] || { echo "Release archive contains no dSYM bundles." >&2; exit 1; }
|
||||||
|
ditto -c -k --sequesterRsrc --keepParent "$IOS_ARCHIVE_PATH/dSYMs" "$artifact/TruckWash-$IOS_MARKETING_VERSION-$IOS_BUILD_NUMBER.dSYM.zip"
|
||||||
|
node scripts/mobile/create-ios-release-manifest.mjs
|
||||||
|
(cd "$artifact" && shasum -a 256 -- * > SHA256SUMS)
|
||||||
|
|
||||||
|
- name: Upload signed IPA
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: truck-wash-ios-${{ env.IOS_SOURCE_SHA }}
|
||||||
|
path: output/ios-release/*.ipa
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Upload release manifest, dSYM, and checksums
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: ios-release-manifest-${{ env.IOS_SOURCE_SHA }}
|
||||||
|
path: |
|
||||||
|
output/ios-release/ios-release-manifest.json
|
||||||
|
output/ios-release/*.dSYM.zip
|
||||||
|
output/ios-release/SHA256SUMS
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 90
|
||||||
|
|
||||||
|
- name: Clean up Apple signing material
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ -n "${IOS_KEYCHAIN_PATH:-}" ]]; then security delete-keychain "$IOS_KEYCHAIN_PATH" || true; fi
|
||||||
|
if [[ -n "${IOS_PROFILE_INSTALL_PATH:-}" ]]; then rm -f "$IOS_PROFILE_INSTALL_PATH"; fi
|
||||||
|
rm -f "$RUNNER_TEMP/apple-distribution.p12" "$RUNNER_TEMP/app-store.mobileprovision" "$RUNNER_TEMP/app-store-profile.plist"
|
||||||
|
|
||||||
|
disabled:
|
||||||
|
name: Automation disabled
|
||||||
|
needs: prepare
|
||||||
|
if: needs.prepare.outputs.enabled != 'true'
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- run: echo "App Store automation is disabled; no signing environment or secrets were accessed."
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
name: Android Store Artifacts
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version_name:
|
||||||
|
description: Store version name, for example 1.4.0
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
version_code:
|
||||||
|
description: Store build number/version code
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
upload_android_to_play:
|
||||||
|
description: Upload the signed Android App Bundle to Google Play
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
android_track:
|
||||||
|
description: Google Play track for manual dispatches
|
||||||
|
required: false
|
||||||
|
type: choice
|
||||||
|
default: production
|
||||||
|
options:
|
||||||
|
- production
|
||||||
|
- beta
|
||||||
|
- alpha
|
||||||
|
- internal
|
||||||
|
android_release_status:
|
||||||
|
description: Google Play release status for manual dispatches
|
||||||
|
required: false
|
||||||
|
type: choice
|
||||||
|
default: inProgress
|
||||||
|
options:
|
||||||
|
- inProgress
|
||||||
|
- draft
|
||||||
|
- halted
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "mobile-v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: android-store-artifacts-${{ github.ref_name || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
android:
|
||||||
|
name: Android AAB and Play upload
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
environment: mobile-store-production
|
||||||
|
timeout-minutes: 60
|
||||||
|
env:
|
||||||
|
ANDROID_PACKAGE_NAME: ${{ vars.ANDROID_PACKAGE_NAME || 'io.truckwash.twa' }}
|
||||||
|
ANDROID_AAB_PATH: ${{ vars.ANDROID_AAB_PATH || 'android/app/build/outputs/bundle/release/app-release.aab' }}
|
||||||
|
ANDROID_SIGNING_IDENTITY_REF: github-environment:mobile-store-production/android-keystore
|
||||||
|
PLAY_STORE_TRACK: ${{ inputs.android_track || vars.PLAY_STORE_TRACK || 'production' }}
|
||||||
|
PLAY_STORE_RELEASE_STATUS: ${{ inputs.android_release_status || 'inProgress' }}
|
||||||
|
PLAY_STORE_USER_FRACTION: "0.01"
|
||||||
|
UPLOAD_ANDROID_TO_PLAY: ${{ github.event_name != 'workflow_dispatch' || inputs.upload_android_to_play }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.sha }}
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Guard current master release
|
||||||
|
id: release-guard
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
|
EXPECTED_SHA: ${{ github.sha }}
|
||||||
|
RELEASE_BRANCH: ${{ github.ref_name }}
|
||||||
|
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||||
|
UPLOAD_TO_PLAY: ${{ github.event_name != 'workflow_dispatch' || inputs.upload_android_to_play }}
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
current=true
|
||||||
|
latest_sha="$(curl --fail --silent --show-error --location \
|
||||||
|
-H "Authorization: Bearer $GH_TOKEN" \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/$DEFAULT_BRANCH" | jq -r '.object.sha // empty')"
|
||||||
|
if [[ ! "$latest_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||||
|
echo "Could not resolve origin/$DEFAULT_BRANCH." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$latest_sha" != "$EXPECTED_SHA" && "$UPLOAD_TO_PLAY" == "true" ]]; then
|
||||||
|
current=false
|
||||||
|
echo "Skipping stale mobile upload for $EXPECTED_SHA; origin/$DEFAULT_BRANCH is $latest_sha."
|
||||||
|
elif [[ "$latest_sha" != "$EXPECTED_SHA" ]]; then
|
||||||
|
echo "Allowing artifact-only build for $EVENT_NAME on $RELEASE_BRANCH; store upload remains disabled."
|
||||||
|
else
|
||||||
|
echo "Mobile upload commit is current for $DEFAULT_BRANCH."
|
||||||
|
fi
|
||||||
|
echo "current=$current" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "source_sha=$latest_sha" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
if: steps.release-guard.outputs.current == 'true'
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Require green Chromium mobile tests before Play upload
|
||||||
|
if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true'
|
||||||
|
run: node scripts/mobile/verify-store-test-gate.mjs --platform android
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
STORE_SOURCE_SHA: ${{ steps.release-guard.outputs.source_sha }}
|
||||||
|
TEST_WORKFLOW_RUN_ID: ""
|
||||||
|
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
if: steps.release-guard.outputs.current == 'true'
|
||||||
|
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: 21
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
if: steps.release-guard.outputs.current == 'true'
|
||||||
|
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
||||||
|
|
||||||
|
- name: Install Android SDK packages
|
||||||
|
if: steps.release-guard.outputs.current == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
yes | sdkmanager --licenses >/dev/null || true
|
||||||
|
sdkmanager "platforms;android-36" "build-tools;36.0.0"
|
||||||
|
|
||||||
|
- name: Resolve mobile version
|
||||||
|
if: steps.release-guard.outputs.current == 'true'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
INPUT_VERSION_NAME: ${{ inputs.version_name || '' }}
|
||||||
|
INPUT_VERSION_CODE: ${{ inputs.version_code || '' }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
version_name="$INPUT_VERSION_NAME"
|
||||||
|
if [[ -z "$version_name" && "$GITHUB_REF_NAME" == mobile-v* ]]; then
|
||||||
|
version_name="${GITHUB_REF_NAME#mobile-v}"
|
||||||
|
fi
|
||||||
|
if [[ -z "$version_name" ]]; then
|
||||||
|
version_name="0.0.${GITHUB_RUN_NUMBER}"
|
||||||
|
fi
|
||||||
|
version_code="${INPUT_VERSION_CODE:-$GITHUB_RUN_NUMBER}"
|
||||||
|
echo "MOBILE_VERSION_NAME=$version_name" >> "$GITHUB_ENV"
|
||||||
|
echo "MOBILE_VERSION_CODE=$version_code" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Check Android store environment
|
||||||
|
if: steps.release-guard.outputs.current == 'true'
|
||||||
|
env:
|
||||||
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64 }}
|
||||||
|
run: node scripts/mobile/check-store-upload-env.mjs --android
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.release-guard.outputs.current == 'true'
|
||||||
|
run: npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
- name: Decode Android signing key
|
||||||
|
if: steps.release-guard.outputs.current == 'true'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
keystore_path="$RUNNER_TEMP/android-release.keystore"
|
||||||
|
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.ANDROID_KEYSTORE_BASE64, 'base64'))" "$keystore_path"
|
||||||
|
echo "ANDROID_KEYSTORE_FILE=$keystore_path" >> "$GITHUB_ENV"
|
||||||
|
echo "ANDROID_KEYSTORE_PASSWORD=$ANDROID_KEYSTORE_PASSWORD" >> "$GITHUB_ENV"
|
||||||
|
echo "ANDROID_KEY_ALIAS=$ANDROID_KEY_ALIAS" >> "$GITHUB_ENV"
|
||||||
|
echo "ANDROID_KEY_PASSWORD=$ANDROID_KEY_PASSWORD" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Build and sync Android shell
|
||||||
|
if: steps.release-guard.outputs.current == 'true'
|
||||||
|
run: |
|
||||||
|
npm run mobile:android:sync
|
||||||
|
npm run mobile:permissions:check
|
||||||
|
npm run mobile:android:signing:check
|
||||||
|
|
||||||
|
- name: Build signed Android App Bundle
|
||||||
|
if: steps.release-guard.outputs.current == 'true'
|
||||||
|
working-directory: android
|
||||||
|
run: ./gradlew --no-daemon bundleRelease
|
||||||
|
|
||||||
|
- name: Verify Android App Bundle signature
|
||||||
|
if: steps.release-guard.outputs.current == 'true'
|
||||||
|
run: jarsigner -verify -certs -verbose "$ANDROID_AAB_PATH" >/dev/null
|
||||||
|
|
||||||
|
- name: Record immutable Android artifact proof
|
||||||
|
if: steps.release-guard.outputs.current == 'true'
|
||||||
|
id: artifact-proof
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
artifact_sha256="$(sha256sum "$ANDROID_AAB_PATH" | awk '{print $1}')"
|
||||||
|
[[ "$artifact_sha256" =~ ^[0-9a-f]{64}$ ]]
|
||||||
|
echo "ANDROID_AAB_SHA256=$artifact_sha256" >> "$GITHUB_ENV"
|
||||||
|
echo "sha256=$artifact_sha256" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Android AAB SHA-256: \`$artifact_sha256\`" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
- name: Upload Android artifact
|
||||||
|
if: steps.release-guard.outputs.current == 'true'
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: truck-wash-android-${{ env.MOBILE_VERSION_NAME }}-${{ github.sha }}
|
||||||
|
path: ${{ env.ANDROID_AAB_PATH }}
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
- name: Recheck live master before Play upload
|
||||||
|
if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true'
|
||||||
|
env:
|
||||||
|
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||||
|
EXPECTED_SHA: ${{ steps.release-guard.outputs.source_sha }}
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
live_master_sha="$(curl --fail --silent --show-error --location \
|
||||||
|
-H "Authorization: Bearer $GH_TOKEN" \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/git/ref/heads/$DEFAULT_BRANCH" | jq -r '.object.sha // empty')"
|
||||||
|
[[ "$live_master_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "Could not resolve origin/$DEFAULT_BRANCH." >&2; exit 1; }
|
||||||
|
[[ "$live_master_sha" == "$EXPECTED_SHA" ]] || {
|
||||||
|
echo "$DEFAULT_BRANCH advanced while the Android bundle was building; refusing Play upload." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Upload Android App Bundle to Google Play
|
||||||
|
if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true'
|
||||||
|
id: play-upload
|
||||||
|
env:
|
||||||
|
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64 }}
|
||||||
|
run: npm run mobile:android:play-upload
|
||||||
|
|
||||||
|
- name: Record Google Play submission proof
|
||||||
|
if: steps.release-guard.outputs.current == 'true' && env.UPLOAD_ANDROID_TO_PLAY == 'true'
|
||||||
|
env:
|
||||||
|
ARTIFACT_SHA256: ${{ steps.artifact-proof.outputs.sha256 }}
|
||||||
|
PLAY_EDIT_ID: ${{ steps.play-upload.outputs.play_edit_id }}
|
||||||
|
PLAY_VERSION_CODE: ${{ steps.play-upload.outputs.version_code }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
test -n "$ARTIFACT_SHA256"
|
||||||
|
test -n "$PLAY_EDIT_ID"
|
||||||
|
test -n "$PLAY_VERSION_CODE"
|
||||||
|
printf 'Google Play submission proof: platform=android applicationId=%s version=%s buildNumber=%s artifactSha256=%s signingIdentityRef=%s storeSubmissionId=%s status=%s fraction=%s\n' \
|
||||||
|
"$ANDROID_PACKAGE_NAME" "$MOBILE_VERSION_NAME" "$PLAY_VERSION_CODE" \
|
||||||
|
"$ARTIFACT_SHA256" "$ANDROID_SIGNING_IDENTITY_REF" "$PLAY_EDIT_ID" \
|
||||||
|
"$PLAY_STORE_RELEASE_STATUS" "$PLAY_STORE_USER_FRACTION" >> "$GITHUB_STEP_SUMMARY"
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
name: Frontend Release Recovery
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
action:
|
||||||
|
description: Verify the active release or roll back before verification
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- reverify
|
||||||
|
- rollback
|
||||||
|
source_sha:
|
||||||
|
description: Exact 40-character commit SHA expected after recovery
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
rollback_target:
|
||||||
|
description: Immutable releases/.../dist target; required for rollback
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: frontend-production
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
recover:
|
||||||
|
name: Protected production recovery
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: frontend-production
|
||||||
|
timeout-minutes: 35
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_BASE_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
|
||||||
|
steps:
|
||||||
|
- name: Validate exact recovery target
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
RECOVERY_ACTION: ${{ inputs.action }}
|
||||||
|
RECOVERY_SHA: ${{ inputs.source_sha }}
|
||||||
|
RECOVERY_TARGET: ${{ inputs.rollback_target }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
[[ "$RECOVERY_SHA" =~ ^[a-f0-9]{40}$ ]]
|
||||||
|
if [[ "$RECOVERY_ACTION" == "rollback" ]]; then
|
||||||
|
[[ "$RECOVERY_TARGET" =~ ^releases/[A-Za-z0-9._-]+/dist$ ]]
|
||||||
|
else
|
||||||
|
[[ -z "$RECOVERY_TARGET" ]]
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Checkout exact recovery source
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
ref: ${{ inputs.source_sha }}
|
||||||
|
|
||||||
|
- name: Authorize source from successful release proof
|
||||||
|
id: authorize
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
RECOVERY_ACTION: ${{ inputs.action }}
|
||||||
|
RECOVERY_SHA: ${{ inputs.source_sha }}
|
||||||
|
RECOVERY_TARGET: ${{ inputs.rollback_target }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
runs="$RUNNER_TEMP/recovery-runs.json"
|
||||||
|
artifacts="$RUNNER_TEMP/recovery-artifacts.json"
|
||||||
|
curl --fail --silent --show-error \
|
||||||
|
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||||
|
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/workflows/release.yml/runs?head_sha=$RECOVERY_SHA&status=success&per_page=20" \
|
||||||
|
> "$runs"
|
||||||
|
release_run_id="$(jq -r '[.workflow_runs[] | select(.event == "workflow_run")] | first | .id // empty' "$runs")"
|
||||||
|
[[ "$release_run_id" =~ ^[0-9]+$ ]]
|
||||||
|
artifact_name="frontend-release-proof-$RECOVERY_SHA"
|
||||||
|
curl --fail --silent --show-error \
|
||||||
|
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||||
|
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/runs/$release_run_id/artifacts?name=$artifact_name&per_page=20" \
|
||||||
|
> "$artifacts"
|
||||||
|
artifact_id="$(jq -r '[.artifacts[] | select(.expired == false)] | first | .id // empty' "$artifacts")"
|
||||||
|
[[ "$artifact_id" =~ ^[0-9]+$ ]]
|
||||||
|
mkdir -p "$RUNNER_TEMP/recovery-proof"
|
||||||
|
curl --fail --silent --show-error --location \
|
||||||
|
-H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||||
|
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/artifacts/$artifact_id/zip" \
|
||||||
|
-o "$RUNNER_TEMP/recovery-proof.zip"
|
||||||
|
unzip -q "$RUNNER_TEMP/recovery-proof.zip" -d "$RUNNER_TEMP/recovery-proof"
|
||||||
|
PROOF_PATH="$RUNNER_TEMP/recovery-proof/frontend-release-proof.json" \
|
||||||
|
RELEASE_RUN_ID="$release_run_id" node <<'NODE'
|
||||||
|
const { appendFileSync, readFileSync } = require("node:fs");
|
||||||
|
const proof = JSON.parse(readFileSync(process.env.PROOF_PATH, "utf8"));
|
||||||
|
const sha = process.env.RECOVERY_SHA;
|
||||||
|
const target = process.env.RECOVERY_TARGET;
|
||||||
|
const expectedPrefix = `releases/${sha}-`;
|
||||||
|
const valid = proof.schemaVersion === 2
|
||||||
|
&& proof.repository === process.env.GITHUB_REPOSITORY
|
||||||
|
&& proof.sha === sha
|
||||||
|
&& proof.sourceSha === sha
|
||||||
|
&& proof.frontendReleaseRunId === process.env.RELEASE_RUN_ID
|
||||||
|
&& proof.verificationState === "verified"
|
||||||
|
&& proof.livePublicGate === "passed"
|
||||||
|
&& ["passed", "not-configured"].includes(proof.liveCredentialedGate)
|
||||||
|
&& proof.releaseManagerGate === "passed"
|
||||||
|
&& proof.serverVersionUpdated === true
|
||||||
|
&& proof.serverVersionReadBack === "passed"
|
||||||
|
&& /^[1-9][0-9]*-[1-9][0-9]*$/.test(String(proof.buildId || ""))
|
||||||
|
&& typeof proof.activeTarget === "string"
|
||||||
|
&& proof.activeTarget.startsWith(expectedPrefix)
|
||||||
|
&& proof.activeTarget.endsWith("/dist");
|
||||||
|
if (!valid) throw new Error("Recovery source does not have valid exact-release proof.");
|
||||||
|
if (process.env.RECOVERY_ACTION === "rollback" && target !== proof.activeTarget) {
|
||||||
|
throw new Error("Rollback target does not match the verified release proof.");
|
||||||
|
}
|
||||||
|
appendFileSync(process.env.GITHUB_OUTPUT, `verified_target=${proof.activeTarget}\n`);
|
||||||
|
appendFileSync(process.env.GITHUB_OUTPUT, `build_id=${proof.buildId}\n`);
|
||||||
|
NODE
|
||||||
|
|
||||||
|
- name: Capture current immutable target
|
||||||
|
id: current
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
|
||||||
|
run: |
|
||||||
|
node --input-type=module <<'NODE'
|
||||||
|
import { appendFileSync } from "node:fs";
|
||||||
|
const response = await fetch(new URL(`release-manifest.json?recovery=${Date.now()}`, process.env.FRONTEND_URL), {
|
||||||
|
headers: { "Cache-Control": "no-cache", Pragma: "no-cache" },
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`Active manifest returned HTTP ${response.status}.`);
|
||||||
|
const manifest = await response.json();
|
||||||
|
const sha = String(manifest.commit_sha || "").toLowerCase();
|
||||||
|
const build = String(manifest.build_id || "");
|
||||||
|
if (!/^[a-f0-9]{40}$/.test(sha) || !/^[A-Za-z0-9._-]{1,180}$/.test(build)) {
|
||||||
|
throw new Error("Active manifest has invalid release identity.");
|
||||||
|
}
|
||||||
|
if (!/^[1-9][0-9]*-[1-9][0-9]*$/.test(build)) {
|
||||||
|
throw new Error("Active manifest build id is not a release run identity.");
|
||||||
|
}
|
||||||
|
appendFileSync(process.env.GITHUB_OUTPUT, `previous_sha=${sha}\nprevious_build_id=${build}\nprevious_target=releases/${sha}-${build}/dist\n`);
|
||||||
|
NODE
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
- name: Install secure FTP client without system changes
|
||||||
|
run: |
|
||||||
|
if command -v lftp >/dev/null 2>&1; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
package_root="$RUNNER_TEMP/lftp-package"
|
||||||
|
mkdir -p "$package_root"
|
||||||
|
(
|
||||||
|
cd "$package_root"
|
||||||
|
apt-get download lftp
|
||||||
|
dpkg-deb --extract ./lftp_*.deb root
|
||||||
|
)
|
||||||
|
echo "$package_root/root/usr/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Install Playwright Chromium
|
||||||
|
run: node scripts/install-playwright-browsers.mjs chromium
|
||||||
|
|
||||||
|
- name: Roll back atomically
|
||||||
|
if: inputs.action == 'rollback'
|
||||||
|
id: rollback
|
||||||
|
run: node scripts/release/deploy-cpanel.mjs --rollback
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --use-system-ca
|
||||||
|
RELEASE_ROLLBACK_TARGET: ${{ inputs.rollback_target }}
|
||||||
|
PRODUCTION_FTP_HOST: ${{ secrets.PRODUCTION_FTP_HOST }}
|
||||||
|
PRODUCTION_FTP_USER: ${{ secrets.PRODUCTION_FTP_USER }}
|
||||||
|
PRODUCTION_FTP_PASSWORD: ${{ secrets.PRODUCTION_FTP_PASSWORD }}
|
||||||
|
PRODUCTION_FTP_PATH: ${{ secrets.PRODUCTION_FTP_PATH }}
|
||||||
|
PRODUCTION_ACTIVATION_KEY: ${{ secrets.PRODUCTION_ACTIVATION_KEY }}
|
||||||
|
PRODUCTION_FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
|
||||||
|
|
||||||
|
- name: Verify active manifest matches authorized release
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
EXPECTED_SHA: ${{ inputs.source_sha }}
|
||||||
|
EXPECTED_TARGET: ${{ steps.authorize.outputs.verified_target }}
|
||||||
|
FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
|
||||||
|
run: |
|
||||||
|
node --input-type=module <<'NODE'
|
||||||
|
const deadline = Date.now() + 300_000;
|
||||||
|
let actual = "";
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
const response = await fetch(new URL(`release-manifest.json?recovery=${Date.now()}`, process.env.FRONTEND_URL), {
|
||||||
|
headers: { "Cache-Control": "no-cache", Pragma: "no-cache" },
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const manifest = await response.json();
|
||||||
|
const manifestSha = String(manifest.commit_sha || "").toLowerCase();
|
||||||
|
actual = `releases/${manifestSha}-${String(manifest.build_id || "")}/dist`;
|
||||||
|
if (manifestSha === process.env.EXPECTED_SHA && actual === process.env.EXPECTED_TARGET) process.exit(0);
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5_000));
|
||||||
|
}
|
||||||
|
throw new Error(`Active release identity did not converge to the authorized target; observed ${actual || "unavailable"}.`);
|
||||||
|
NODE
|
||||||
|
|
||||||
|
- name: Public live verification
|
||||||
|
run: npm run test:e2e:live:public
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --use-system-ca
|
||||||
|
|
||||||
|
- name: Credentialed live verification
|
||||||
|
run: npm run test:e2e:live:roles
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --use-system-ca
|
||||||
|
PLAYWRIGHT_REQUIRE_LIVE_CREDENTIALS: "true"
|
||||||
|
PLAYWRIGHT_USER_CUSTOMER_NUMBER: ${{ secrets.PLAYWRIGHT_USER_CUSTOMER_NUMBER }}
|
||||||
|
PLAYWRIGHT_USER_PASSWORD: ${{ secrets.PLAYWRIGHT_USER_PASSWORD }}
|
||||||
|
PLAYWRIGHT_USER_OTP_SECRET: ${{ secrets.PLAYWRIGHT_USER_OTP_SECRET }}
|
||||||
|
PLAYWRIGHT_OPERATOR_USER_ID: ${{ secrets.PLAYWRIGHT_OPERATOR_USER_ID }}
|
||||||
|
PLAYWRIGHT_OPERATOR_PASSWORD: ${{ secrets.PLAYWRIGHT_OPERATOR_PASSWORD }}
|
||||||
|
PLAYWRIGHT_DEPARTMENT_ID: ${{ secrets.PLAYWRIGHT_DEPARTMENT_ID }}
|
||||||
|
|
||||||
|
- name: Record verified server version
|
||||||
|
run: npm run release:update-server-version
|
||||||
|
env:
|
||||||
|
SERVER_UPDATE_TOKEN: ${{ secrets.SERVER_UPDATE_TOKEN }}
|
||||||
|
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
|
||||||
|
RELEASE_VERSION: ${{ inputs.source_sha }}
|
||||||
|
RELEASE_BUILD_ID: ${{ steps.authorize.outputs.build_id }}
|
||||||
|
RELEASE_VERSION_UPDATE_REQUIRED: "true"
|
||||||
|
|
||||||
|
- name: Publish recovery audit
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: frontend-release-recovery-${{ inputs.source_sha }}-${{ github.run_id }}
|
||||||
|
path: |
|
||||||
|
test-results
|
||||||
|
playwright-report
|
||||||
|
if-no-files-found: ignore
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Restore pre-recovery target after downstream failure
|
||||||
|
if: >-
|
||||||
|
failure() && inputs.action == 'rollback'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
node scripts/release/deploy-cpanel.mjs --rollback
|
||||||
|
node --input-type=module <<'NODE'
|
||||||
|
const deadline = Date.now() + 300_000;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
const response = await fetch(new URL(`release-manifest.json?restore=${Date.now()}`, process.env.PRODUCTION_FRONTEND_URL), {
|
||||||
|
headers: { "Cache-Control": "no-cache", Pragma: "no-cache" },
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const manifest = await response.json();
|
||||||
|
const sha = String(manifest.commit_sha || "").toLowerCase();
|
||||||
|
const target = `releases/${sha}-${String(manifest.build_id || "")}/dist`;
|
||||||
|
if (sha === process.env.RELEASE_VERSION && target === process.env.RELEASE_ROLLBACK_TARGET) process.exit(0);
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5_000));
|
||||||
|
}
|
||||||
|
throw new Error("Failed to restore and verify the pre-recovery target.");
|
||||||
|
NODE
|
||||||
|
npm run release:update-server-version
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --use-system-ca
|
||||||
|
RELEASE_ROLLBACK_TARGET: ${{ steps.current.outputs.previous_target }}
|
||||||
|
RELEASE_VERSION: ${{ steps.current.outputs.previous_sha }}
|
||||||
|
RELEASE_BUILD_ID: ${{ steps.current.outputs.previous_build_id }}
|
||||||
|
RELEASE_VERSION_UPDATE_REQUIRED: "true"
|
||||||
|
PRODUCTION_FTP_HOST: ${{ secrets.PRODUCTION_FTP_HOST }}
|
||||||
|
PRODUCTION_FTP_USER: ${{ secrets.PRODUCTION_FTP_USER }}
|
||||||
|
PRODUCTION_FTP_PASSWORD: ${{ secrets.PRODUCTION_FTP_PASSWORD }}
|
||||||
|
PRODUCTION_FTP_PATH: ${{ secrets.PRODUCTION_FTP_PATH }}
|
||||||
|
PRODUCTION_ACTIVATION_KEY: ${{ secrets.PRODUCTION_ACTIVATION_KEY }}
|
||||||
|
PRODUCTION_FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
|
||||||
|
SERVER_UPDATE_TOKEN: ${{ secrets.SERVER_UPDATE_TOKEN }}
|
||||||
|
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
|
||||||
@@ -1,41 +1,198 @@
|
|||||||
name: Frontend Release
|
name: Frontend Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- Automated Tests
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: read
|
actions: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: frontend-release-${{ github.ref }}
|
group: frontend-production
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-upload-and-verify:
|
build-release:
|
||||||
runs-on: [self-hosted, Linux, X64, default]
|
if: >-
|
||||||
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.event.workflow_run.event == 'push' &&
|
||||||
|
github.event.workflow_run.head_branch == 'master' &&
|
||||||
|
github.event.workflow_run.head_repository.full_name == github.repository
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
env:
|
env:
|
||||||
RELEASE_BASE_URL: https://api-v2.truckwash.io/master/frontend
|
RELEASE_COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||||
PLAYWRIGHT_BASE_URL: https://api-v2.truckwash.io/master/frontend
|
RELEASE_EXPECTED_COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||||
PLAYWRIGHT_RELEASE_API_BASE_URL: https://api-v2.truckwash.io
|
|
||||||
PLAYWRIGHT_RELEASE_API_PING_PATHS: /ping,/master/api/ping,/canary/api/ping,/stable/api/ping
|
|
||||||
RELEASE_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
|
RELEASE_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
RELEASE_EXPECTED_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
|
outputs:
|
||||||
RELEASE_EXPECTED_COMMIT: ${{ github.sha }}
|
current: ${{ steps.branch-head.outputs.current }}
|
||||||
RELEASE_WAIT_INITIAL_SECONDS: 45
|
build_id: ${{ steps.package.outputs.build_id }}
|
||||||
RELEASE_WAIT_TIMEOUT_SECONDS: 600
|
artifact_name: ${{ steps.package-names.outputs.artifact_name }}
|
||||||
RELEASE_POLL_INTERVAL_SECONDS: 10
|
archive_name: ${{ steps.package.outputs.archive_name }}
|
||||||
|
checksum_name: ${{ steps.package-names.outputs.checksum_name }}
|
||||||
|
inventory_name: ${{ steps.package-names.outputs.inventory_name }}
|
||||||
|
release_id: ${{ steps.package.outputs.release_id }}
|
||||||
|
archive_sha256: ${{ steps.package.outputs.archive_sha256 }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Check release commit is current
|
||||||
uses: actions/checkout@v5
|
id: branch-head
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
script: |
|
||||||
|
const { data: branch } = await github.rest.repos.getBranch({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
branch: "master",
|
||||||
|
});
|
||||||
|
const expected = process.env.RELEASE_EXPECTED_COMMIT;
|
||||||
|
const current = branch.commit.sha === expected;
|
||||||
|
core.setOutput("current", String(current));
|
||||||
|
core.info(
|
||||||
|
current
|
||||||
|
? `Release commit ${expected} is current for master.`
|
||||||
|
: `Skipping stale release for ${expected}; origin/master is ${branch.commit.sha}.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
- name: Checkout tested commit
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
ref: ${{ env.RELEASE_COMMIT_SHA }}
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v5
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
run: npm ci --legacy-peer-deps
|
||||||
|
|
||||||
|
- name: Check AI workflow sync
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
run: node scripts/sync-ai-workflow.mjs --check
|
||||||
|
|
||||||
|
- name: Source and i18n checks
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
run: |
|
||||||
|
npm run text:check-encoding
|
||||||
|
npm run i18n:v2:source-check
|
||||||
|
|
||||||
|
- name: Build release artifact
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Record pre-gate dist inventory
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
run: |
|
||||||
|
inventory="$RUNNER_TEMP/dist-before-production-gate.txt"
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
relative_path="${file#dist/}"
|
||||||
|
printf '%s\t%s\t%s\n' \
|
||||||
|
"$(sha256sum "$file" | awk '{print $1}')" \
|
||||||
|
"$(stat --format='%s' "$file")" \
|
||||||
|
"$relative_path"
|
||||||
|
done < <(find dist -type f -print0 | LC_ALL=C sort -z) > "$inventory"
|
||||||
|
|
||||||
|
- name: Install Playwright Chromium
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
run: node scripts/install-playwright-browsers.mjs chromium
|
||||||
|
|
||||||
|
- name: Production Playwright gate
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
run: npm run test:e2e:prod
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_PROD_PREBUILT: "1"
|
||||||
|
PLAYWRIGHT_PROD_WEBKIT: "0"
|
||||||
|
|
||||||
|
- name: Confirm production gate did not mutate dist
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
run: |
|
||||||
|
inventory="$RUNNER_TEMP/dist-after-production-gate.txt"
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
relative_path="${file#dist/}"
|
||||||
|
printf '%s\t%s\t%s\n' \
|
||||||
|
"$(sha256sum "$file" | awk '{print $1}')" \
|
||||||
|
"$(stat --format='%s' "$file")" \
|
||||||
|
"$relative_path"
|
||||||
|
done < <(find dist -type f -print0 | LC_ALL=C sort -z) > "$inventory"
|
||||||
|
cmp "$RUNNER_TEMP/dist-before-production-gate.txt" "$inventory"
|
||||||
|
|
||||||
|
- name: Package and validate release
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
id: package
|
||||||
|
run: node scripts/release/package-dist.mjs
|
||||||
|
env:
|
||||||
|
RELEASE_OUTPUT_DIR: release-artifacts
|
||||||
|
|
||||||
|
- name: Resolve package metadata
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
id: package-names
|
||||||
|
env:
|
||||||
|
BUILD_ID: ${{ steps.package.outputs.build_id }}
|
||||||
|
CHECKSUM_PATH: ${{ steps.package.outputs.checksum_path }}
|
||||||
|
INVENTORY_PATH: ${{ steps.package.outputs.inventory_path }}
|
||||||
|
run: |
|
||||||
|
echo "artifact_name=frontend-release-$BUILD_ID" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "checksum_name=$(basename -- "$CHECKSUM_PATH")" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "inventory_name=$(basename -- "$INVENTORY_PATH")" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Upload release package
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: ${{ steps.package-names.outputs.artifact_name }}
|
||||||
|
path: |
|
||||||
|
${{ steps.package.outputs.archive_path }}
|
||||||
|
${{ steps.package.outputs.checksum_path }}
|
||||||
|
${{ steps.package.outputs.inventory_path }}
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
deploy-frontend-production:
|
||||||
|
needs: build-release
|
||||||
|
if: needs.build-release.outputs.current == 'true'
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 90
|
||||||
|
environment:
|
||||||
|
name: frontend-production
|
||||||
|
url: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
|
||||||
|
env:
|
||||||
|
RELEASE_BASE_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
|
||||||
|
PLAYWRIGHT_BASE_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
|
||||||
|
PLAYWRIGHT_RELEASE_STATIC_BASE_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
|
||||||
|
PLAYWRIGHT_RELEASE_API_BASE_URL: https://api-v2.truckwash.io
|
||||||
|
PLAYWRIGHT_RELEASE_API_PING_PATHS: /master/api/ping
|
||||||
|
RELEASE_COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
RELEASE_EXPECTED_COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
RELEASE_BUILD_ID: ${{ needs.build-release.outputs.build_id }}
|
||||||
|
RELEASE_EXPECTED_BUILD_ID: ${{ needs.build-release.outputs.build_id }}
|
||||||
|
RELEASE_ID: ${{ needs.build-release.outputs.release_id }}
|
||||||
|
RELEASE_STRICT_BUILD_ID: "true"
|
||||||
|
RELEASE_REQUIRE_CACHE_HEADERS: "true"
|
||||||
|
RELEASE_WAIT_INITIAL_SECONDS: 0
|
||||||
|
RELEASE_WAIT_TIMEOUT_SECONDS: 300
|
||||||
|
RELEASE_POLL_INTERVAL_SECONDS: 5
|
||||||
|
steps:
|
||||||
|
- name: Checkout tested commit
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
ref: ${{ env.RELEASE_COMMIT_SHA }}
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -43,48 +200,120 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --legacy-peer-deps
|
run: npm ci --legacy-peer-deps
|
||||||
|
|
||||||
- name: Check AI workflow sync
|
- name: Install secure FTP client without system changes
|
||||||
run: node scripts/sync-ai-workflow.mjs --check
|
|
||||||
|
|
||||||
- name: Source and i18n checks
|
|
||||||
run: |
|
run: |
|
||||||
npm run text:check-encoding
|
if command -v lftp >/dev/null 2>&1; then
|
||||||
npm run i18n:v2:source-check
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Unit tests
|
package_root="$RUNNER_TEMP/lftp-package"
|
||||||
run: npm run test:unit
|
mkdir -p "$package_root"
|
||||||
env:
|
(
|
||||||
VITEST_BATCH_SIZE: 5
|
cd "$package_root"
|
||||||
|
apt-get download lftp
|
||||||
- name: Build release artifact
|
dpkg-deb --extract ./lftp_*.deb root
|
||||||
run: npm run build
|
)
|
||||||
|
echo "$package_root/root/usr/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Install Playwright Chromium
|
- name: Install Playwright Chromium
|
||||||
run: npx playwright install --with-deps chromium
|
run: node scripts/install-playwright-browsers.mjs chromium
|
||||||
|
|
||||||
- name: Production Playwright gate
|
- name: Download validated release package
|
||||||
run: npm run test:e2e:prod
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
|
||||||
- name: Upload dist artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: frontend-dist-${{ env.RELEASE_BUILD_ID }}
|
name: ${{ needs.build-release.outputs.artifact_name }}
|
||||||
path: dist
|
path: release-artifacts
|
||||||
retention-days: 14
|
|
||||||
|
|
||||||
- name: Wait for Coolify release artifact
|
- name: Resolve downloaded release package
|
||||||
run: npm run release:verify-upload
|
env:
|
||||||
|
ARCHIVE_NAME: ${{ needs.build-release.outputs.archive_name }}
|
||||||
|
CHECKSUM_NAME: ${{ needs.build-release.outputs.checksum_name }}
|
||||||
|
INVENTORY_NAME: ${{ needs.build-release.outputs.inventory_name }}
|
||||||
|
run: |
|
||||||
|
[[ -n "$ARCHIVE_NAME" && "$ARCHIVE_NAME" == "$(basename -- "$ARCHIVE_NAME")" ]]
|
||||||
|
[[ -n "$CHECKSUM_NAME" && "$CHECKSUM_NAME" == "$(basename -- "$CHECKSUM_NAME")" ]]
|
||||||
|
[[ -n "$INVENTORY_NAME" && "$INVENTORY_NAME" == "$(basename -- "$INVENTORY_NAME")" ]]
|
||||||
|
|
||||||
|
archive_path="$GITHUB_WORKSPACE/release-artifacts/$ARCHIVE_NAME"
|
||||||
|
checksum_path="$GITHUB_WORKSPACE/release-artifacts/$CHECKSUM_NAME"
|
||||||
|
inventory_path="$GITHUB_WORKSPACE/release-artifacts/$INVENTORY_NAME"
|
||||||
|
[[ -f "$archive_path" && -f "$checksum_path" && -f "$inventory_path" ]]
|
||||||
|
|
||||||
|
echo "RELEASE_ARCHIVE_PATH=$archive_path" >> "$GITHUB_ENV"
|
||||||
|
echo "RELEASE_ARCHIVE_SHA256_PATH=$checksum_path" >> "$GITHUB_ENV"
|
||||||
|
echo "RELEASE_INVENTORY_PATH=$inventory_path" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Check release commit is still current
|
||||||
|
id: branch-head
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
script: |
|
||||||
|
const { data: branch } = await github.rest.repos.getBranch({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
branch: "master",
|
||||||
|
});
|
||||||
|
const expected = process.env.RELEASE_EXPECTED_COMMIT;
|
||||||
|
const current = branch.commit.sha === expected;
|
||||||
|
core.setOutput("current", String(current));
|
||||||
|
core.info(
|
||||||
|
current
|
||||||
|
? `Release commit ${expected} is current immediately before activation.`
|
||||||
|
: `Skipping stale release for ${expected}; origin/master is ${branch.commit.sha}.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
- name: Deploy atomically and verify cPanel release
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
id: deploy
|
||||||
|
timeout-minutes: 15
|
||||||
|
run: node scripts/release/deploy-cpanel.mjs
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --use-system-ca
|
||||||
|
PRODUCTION_FTP_HOST: ${{ secrets.PRODUCTION_FTP_HOST }}
|
||||||
|
PRODUCTION_FTP_USER: ${{ secrets.PRODUCTION_FTP_USER }}
|
||||||
|
PRODUCTION_FTP_PASSWORD: ${{ secrets.PRODUCTION_FTP_PASSWORD }}
|
||||||
|
PRODUCTION_FTP_PATH: ${{ secrets.PRODUCTION_FTP_PATH }}
|
||||||
|
PRODUCTION_ACTIVATION_KEY: ${{ secrets.PRODUCTION_ACTIVATION_KEY }}
|
||||||
|
PRODUCTION_FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
|
||||||
|
RELEASE_GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
|
RELEASE_GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
- name: Public live Playwright gate
|
- name: Public live Playwright gate
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
id: public_live
|
||||||
|
timeout-minutes: 10
|
||||||
run: npm run test:e2e:live:public
|
run: npm run test:e2e:live:public
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --use-system-ca
|
NODE_OPTIONS: --use-system-ca
|
||||||
|
|
||||||
- name: Credentialed live Playwright gate
|
- name: Detect credentialed live gate configuration
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
id: credentialed_live_config
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ -n "$CUSTOMER_NUMBER" && -n "$CUSTOMER_PASSWORD" &&
|
||||||
|
-n "$OPERATOR_USER_ID" && -n "$OPERATOR_PASSWORD" ]]; then
|
||||||
|
echo "configured=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "configured=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
CUSTOMER_NUMBER: ${{ secrets.PLAYWRIGHT_USER_CUSTOMER_NUMBER }}
|
||||||
|
CUSTOMER_PASSWORD: ${{ secrets.PLAYWRIGHT_USER_PASSWORD }}
|
||||||
|
OPERATOR_USER_ID: ${{ secrets.PLAYWRIGHT_OPERATOR_USER_ID }}
|
||||||
|
OPERATOR_PASSWORD: ${{ secrets.PLAYWRIGHT_OPERATOR_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Credentialed live Playwright gate (when configured)
|
||||||
|
if: >-
|
||||||
|
steps.branch-head.outputs.current == 'true' &&
|
||||||
|
steps.credentialed_live_config.outputs.configured == 'true'
|
||||||
|
id: credentialed_live
|
||||||
|
timeout-minutes: 15
|
||||||
run: npm run test:e2e:live:roles
|
run: npm run test:e2e:live:roles
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --use-system-ca
|
NODE_OPTIONS: --use-system-ca
|
||||||
PLAYWRIGHT_REQUIRE_LIVE_CREDENTIALS: "true"
|
|
||||||
PLAYWRIGHT_USER_CUSTOMER_NUMBER: ${{ secrets.PLAYWRIGHT_USER_CUSTOMER_NUMBER }}
|
PLAYWRIGHT_USER_CUSTOMER_NUMBER: ${{ secrets.PLAYWRIGHT_USER_CUSTOMER_NUMBER }}
|
||||||
PLAYWRIGHT_USER_PASSWORD: ${{ secrets.PLAYWRIGHT_USER_PASSWORD }}
|
PLAYWRIGHT_USER_PASSWORD: ${{ secrets.PLAYWRIGHT_USER_PASSWORD }}
|
||||||
PLAYWRIGHT_USER_OTP_SECRET: ${{ secrets.PLAYWRIGHT_USER_OTP_SECRET }}
|
PLAYWRIGHT_USER_OTP_SECRET: ${{ secrets.PLAYWRIGHT_USER_OTP_SECRET }}
|
||||||
@@ -93,29 +322,131 @@ jobs:
|
|||||||
PLAYWRIGHT_DEPARTMENT_ID: ${{ secrets.PLAYWRIGHT_DEPARTMENT_ID }}
|
PLAYWRIGHT_DEPARTMENT_ID: ${{ secrets.PLAYWRIGHT_DEPARTMENT_ID }}
|
||||||
|
|
||||||
- name: Record Release Manager gate
|
- name: Record Release Manager gate
|
||||||
|
id: release_manager
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
timeout-minutes: 5
|
||||||
run: |
|
run: |
|
||||||
test -n "$RELEASE_MANAGER_GATE_TOKEN" || (echo "RELEASE_MANAGER_GATE_TOKEN is required" >&2; exit 1)
|
test -n "$RELEASE_MANAGER_GATE_TOKEN" || (echo "RELEASE_MANAGER_GATE_TOKEN is required" >&2; exit 1)
|
||||||
|
release_gate_build_id="${RELEASE_VERIFIED_BUILD_ID:-$RELEASE_EXPECTED_BUILD_ID}"
|
||||||
curl --fail --show-error --silent \
|
curl --fail --show-error --silent \
|
||||||
-X POST "$RELEASE_MANAGER_GATE_URL" \
|
-X POST "$RELEASE_MANAGER_GATE_URL" \
|
||||||
-H "Authorization: Bearer $RELEASE_MANAGER_GATE_TOKEN" \
|
-H "Authorization: Bearer $RELEASE_MANAGER_GATE_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"$RELEASE_BRANCH\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$RELEASE_EXPECTED_BUILD_ID\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":true,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"static_artifact\",\"api_gateway\"]}"
|
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"master\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$release_gate_build_id\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":false,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"api_gateway\"]}"
|
||||||
env:
|
env:
|
||||||
RELEASE_MANAGER_GATE_URL: ${{ secrets.RELEASE_MANAGER_GATE_URL || 'https://api.truckwash.io/release/gate/test-runs' }}
|
RELEASE_MANAGER_GATE_URL: ${{ secrets.RELEASE_MANAGER_GATE_URL || 'https://api.truckwash.io/release/gate/test-runs' }}
|
||||||
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
|
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
|
||||||
RELEASE_REPOSITORY: ${{ github.repository }}
|
RELEASE_REPOSITORY: ${{ github.repository }}
|
||||||
RELEASE_BRANCH: ${{ github.ref_name }}
|
|
||||||
RELEASE_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
RELEASE_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
|
||||||
- name: Update server version after verification
|
- name: Update server version after verification
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
run: npm run release:update-server-version
|
run: npm run release:update-server-version
|
||||||
env:
|
env:
|
||||||
SERVER_UPDATE_TOKEN: ${{ secrets.SERVER_UPDATE_TOKEN }}
|
SERVER_UPDATE_TOKEN: ${{ secrets.SERVER_UPDATE_TOKEN }}
|
||||||
RELEASE_VERSION: ${{ github.sha }}
|
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
|
||||||
|
RELEASE_VERSION: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
RELEASE_VERSION_UPDATE_REQUIRED: "true"
|
||||||
|
|
||||||
|
- name: Create verified frontend release proof
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
proof_dir="output/frontend-release-proof"
|
||||||
|
mkdir -p "$proof_dir"
|
||||||
|
PROOF_PATH="$proof_dir/frontend-release-proof.json" node <<'NODE'
|
||||||
|
const { writeFileSync } = require("node:fs");
|
||||||
|
const required = (name) => {
|
||||||
|
if (!process.env[name]) throw new Error(`Missing ${name}`);
|
||||||
|
return process.env[name];
|
||||||
|
};
|
||||||
|
const requireSuccessfulStep = (name) => {
|
||||||
|
const outcome = required(name);
|
||||||
|
if (outcome !== "success") throw new Error(`${name} did not succeed: ${outcome}`);
|
||||||
|
return "passed";
|
||||||
|
};
|
||||||
|
const credentialedGate = () => {
|
||||||
|
const configured = required("LIVE_CREDENTIALED_GATE_CONFIGURED");
|
||||||
|
if (configured === "false") return "not-configured";
|
||||||
|
if (configured !== "true") {
|
||||||
|
throw new Error(`Invalid LIVE_CREDENTIALED_GATE_CONFIGURED: ${configured}`);
|
||||||
|
}
|
||||||
|
return requireSuccessfulStep("LIVE_CREDENTIALED_GATE_OUTCOME");
|
||||||
|
};
|
||||||
|
const proof = {
|
||||||
|
schemaVersion: 2,
|
||||||
|
releaseId: required("RELEASE_ID"),
|
||||||
|
sha: required("RELEASE_COMMIT_SHA").toLowerCase(),
|
||||||
|
archiveSha256: required("RELEASE_ARCHIVE_SHA256").toLowerCase(),
|
||||||
|
activeTarget: required("RELEASE_ACTIVE_TARGET"),
|
||||||
|
rollbackTarget: process.env.RELEASE_ROLLBACK_TARGET || null,
|
||||||
|
verificationState: "verified",
|
||||||
|
observedAt: new Date().toISOString(),
|
||||||
|
repository: required("GITHUB_REPOSITORY"),
|
||||||
|
sourceSha: required("RELEASE_COMMIT_SHA").toLowerCase(),
|
||||||
|
testedWorkflowRunId: required("TESTED_WORKFLOW_RUN_ID"),
|
||||||
|
frontendReleaseRunId: required("GITHUB_RUN_ID"),
|
||||||
|
frontendReleaseRunAttempt: required("GITHUB_RUN_ATTEMPT"),
|
||||||
|
buildId: required("RELEASE_BUILD_ID"),
|
||||||
|
livePublicGate: requireSuccessfulStep("LIVE_PUBLIC_GATE_OUTCOME"),
|
||||||
|
liveCredentialedGate: credentialedGate(),
|
||||||
|
releaseManagerGate: requireSuccessfulStep("RELEASE_MANAGER_GATE_OUTCOME"),
|
||||||
|
serverVersionUpdated: true,
|
||||||
|
serverVersionReadBack: "passed",
|
||||||
|
completedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
writeFileSync(process.env.PROOF_PATH, `${JSON.stringify(proof, null, 2)}\n`, { mode: 0o600 });
|
||||||
|
NODE
|
||||||
|
env:
|
||||||
|
TESTED_WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
|
||||||
|
RELEASE_ARCHIVE_SHA256: ${{ needs.build-release.outputs.archive_sha256 }}
|
||||||
|
RELEASE_ACTIVE_TARGET: ${{ steps.deploy.outputs.active_target }}
|
||||||
|
RELEASE_ROLLBACK_TARGET: ${{ steps.deploy.outputs.rollback_target }}
|
||||||
|
LIVE_PUBLIC_GATE_OUTCOME: ${{ steps.public_live.outcome }}
|
||||||
|
LIVE_CREDENTIALED_GATE_CONFIGURED: ${{ steps.credentialed_live_config.outputs.configured }}
|
||||||
|
LIVE_CREDENTIALED_GATE_OUTCOME: ${{ steps.credentialed_live.outcome }}
|
||||||
|
RELEASE_MANAGER_GATE_OUTCOME: ${{ steps.release_manager.outcome }}
|
||||||
|
|
||||||
|
- name: Publish verified frontend release proof
|
||||||
|
if: steps.branch-head.outputs.current == 'true'
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: frontend-release-proof-${{ env.RELEASE_COMMIT_SHA }}
|
||||||
|
path: output/frontend-release-proof/frontend-release-proof.json
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Roll back after any post-deployment verification failure
|
||||||
|
if: >-
|
||||||
|
failure() && steps.branch-head.outputs.current == 'true' &&
|
||||||
|
steps.deploy.outcome == 'success'
|
||||||
|
timeout-minutes: 10
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
node scripts/release/deploy-cpanel.mjs --rollback
|
||||||
|
[[ "$RELEASE_ROLLBACK_TARGET" =~ ^releases/([a-f0-9]{40})-([1-9][0-9]*-[1-9][0-9]*)/dist$ ]]
|
||||||
|
export RELEASE_VERSION="${BASH_REMATCH[1]}"
|
||||||
|
export RELEASE_BUILD_ID="${BASH_REMATCH[2]}"
|
||||||
|
npm run release:update-server-version
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --use-system-ca
|
||||||
|
RELEASE_ROLLBACK_TARGET: ${{ steps.deploy.outputs.rollback_target }}
|
||||||
|
PRODUCTION_FTP_HOST: ${{ secrets.PRODUCTION_FTP_HOST }}
|
||||||
|
PRODUCTION_FTP_USER: ${{ secrets.PRODUCTION_FTP_USER }}
|
||||||
|
PRODUCTION_FTP_PASSWORD: ${{ secrets.PRODUCTION_FTP_PASSWORD }}
|
||||||
|
PRODUCTION_FTP_PATH: ${{ secrets.PRODUCTION_FTP_PATH }}
|
||||||
|
PRODUCTION_ACTIVATION_KEY: ${{ secrets.PRODUCTION_ACTIVATION_KEY }}
|
||||||
|
PRODUCTION_FRONTEND_URL: ${{ vars.PRODUCTION_FRONTEND_URL || 'https://truckwash.io' }}
|
||||||
|
SERVER_UPDATE_TOKEN: ${{ secrets.SERVER_UPDATE_TOKEN }}
|
||||||
|
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
|
||||||
|
RELEASE_VERSION_UPDATE_REQUIRED: "true"
|
||||||
|
|
||||||
- name: Upload Playwright report
|
- name: Upload Playwright report
|
||||||
if: failure()
|
if: failure() && steps.branch-head.outputs.current == 'true'
|
||||||
uses: actions/upload-artifact@v4
|
continue-on-error: true
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: frontend-release-playwright-${{ env.RELEASE_BUILD_ID }}
|
name: frontend-release-playwright-${{ env.RELEASE_BUILD_ID }}
|
||||||
path: output/playwright
|
path: output/playwright
|
||||||
|
|||||||
@@ -3,7 +3,44 @@ name: Automated Tests
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- dev
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
mode:
|
||||||
|
description: "What to run for a manual dispatch."
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
default: full
|
||||||
|
options:
|
||||||
|
- full
|
||||||
|
- targeted
|
||||||
|
- targeted-then-full
|
||||||
|
target_specs:
|
||||||
|
description: "Comma- or newline-separated Playwright spec paths under tests/e2e."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: "tests/e2e/superuser-department-overview.spec.js"
|
||||||
|
target_projects:
|
||||||
|
description: "JSON array of Playwright projects for targeted mode."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: '["chromium-desktop","chromium-mobile","chromium-tablet","webkit-mobile","webkit-desktop"]'
|
||||||
|
target_grep:
|
||||||
|
description: "Optional Playwright grep pattern for targeted mode."
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
runner:
|
||||||
|
description: "Runner pool for this manually dispatched test run"
|
||||||
|
required: false
|
||||||
|
default: "self-hosted"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- self-hosted
|
||||||
|
- github-hosted
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 2 * * *"
|
- cron: "0 2 * * *"
|
||||||
|
|
||||||
@@ -11,22 +48,44 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: frontend-tests-${{ github.workflow }}-${{ github.ref }}
|
group: frontend-tests-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.event_name == 'push' && github.ref || github.run_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
|
||||||
|
|
||||||
|
# Repository variables used as CI runner and credit controls:
|
||||||
|
# - FRONTEND_CI_STANDARD_RUNNER: JSON runs-on value for format/build/unit jobs.
|
||||||
|
# - FRONTEND_CI_E2E_RUNNER: JSON runs-on value for Playwright jobs.
|
||||||
|
# - FRONTEND_CI_PR_E2E_MAX_PARALLEL: numeric Playwright PR job parallelism.
|
||||||
|
# - FRONTEND_CI_FULL_E2E_MAX_PARALLEL: numeric full-suite job parallelism.
|
||||||
|
# - FRONTEND_CI_FULL_E2E_CONCURRENT_MAX_PARALLEL: full-suite parallelism while PR E2E runs beside it.
|
||||||
|
# GitHub-hosted target: ["ubuntu-24.04"], with PR parallelism 10, concurrent full parallelism 26,
|
||||||
|
# and standalone scheduled full parallelism 36. This keeps the workflow peak at 36 hosted jobs.
|
||||||
jobs:
|
jobs:
|
||||||
format-tests:
|
format-tests:
|
||||||
# CI runs on the repository's self-hosted runner pool.
|
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
|
||||||
runs-on: [self-hosted, Linux, X64, default]
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
|
- name: Repair self-hosted workspace permissions
|
||||||
|
if: ${{ contains(vars.FRONTEND_CI_STANDARD_RUNNER || 'self-hosted', 'self-hosted') }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||||
|
sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
|
||||||
|
foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)"
|
||||||
|
if [[ -n "$foreign_entry" ]]; then
|
||||||
|
trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB"
|
||||||
|
rm -rf "$trash" 2>/dev/null || true
|
||||||
|
mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: npm
|
|
||||||
|
|
||||||
- name: Check AI workflow sync
|
- name: Check AI workflow sync
|
||||||
run: node scripts/sync-ai-workflow.mjs --check
|
run: node scripts/sync-ai-workflow.mjs --check
|
||||||
@@ -37,37 +96,310 @@ jobs:
|
|||||||
- name: Check frontend test formatting
|
- name: Check frontend test formatting
|
||||||
run: npm run format:tests:check
|
run: npm run format:tests:check
|
||||||
|
|
||||||
build-and-unit:
|
quality-checks:
|
||||||
needs: format-tests
|
name: Quality-${{ matrix.check }}
|
||||||
runs-on: [self-hosted, Linux, X64, default]
|
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 5
|
||||||
|
matrix:
|
||||||
|
check: [lint, i18n, build, unit-fast, unit-serial]
|
||||||
steps:
|
steps:
|
||||||
|
- name: Repair self-hosted workspace permissions
|
||||||
|
if: ${{ contains(vars.FRONTEND_CI_STANDARD_RUNNER || 'self-hosted', 'self-hosted') }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||||
|
sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
|
||||||
|
foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)"
|
||||||
|
if [[ -n "$foreign_entry" ]]; then
|
||||||
|
trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB"
|
||||||
|
rm -rf "$trash" 2>/dev/null || true
|
||||||
|
mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: npm
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --legacy-peer-deps
|
run: npm ci --legacy-peer-deps
|
||||||
|
|
||||||
- name: Build sanity check
|
- name: Run quality check
|
||||||
run: npm run build
|
shell: bash
|
||||||
|
|
||||||
- name: Unit tests
|
|
||||||
run: npm run test:unit
|
|
||||||
env:
|
env:
|
||||||
VITEST_BATCH_SIZE: 5
|
MATRIX_CHECK: ${{ matrix.check }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
case "$MATRIX_CHECK" in
|
||||||
|
lint)
|
||||||
|
npm run lint
|
||||||
|
;;
|
||||||
|
i18n)
|
||||||
|
npm run i18n:v2:check
|
||||||
|
;;
|
||||||
|
build)
|
||||||
|
npm run build
|
||||||
|
;;
|
||||||
|
unit-fast)
|
||||||
|
npm run text:check-encoding
|
||||||
|
npm run test:unit:fast
|
||||||
|
;;
|
||||||
|
unit-serial)
|
||||||
|
VITEST_BATCH_SIZE=5 npm run test:unit:serial
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported quality check: $MATRIX_CHECK" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
build-and-unit:
|
||||||
|
if: ${{ always() }}
|
||||||
|
name: Build and unit summary
|
||||||
|
needs: [format-tests, quality-checks]
|
||||||
|
runs-on: ${{ fromJSON(vars.FRONTEND_CI_STANDARD_RUNNER || '["self-hosted","Linux","X64","pleno","frontend"]') }}
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Verify quality jobs succeeded
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
FORMAT_TESTS_RESULT: ${{ needs.format-tests.result }}
|
||||||
|
QUALITY_CHECKS_RESULT: ${{ needs.quality-checks.result }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
failed=0
|
||||||
|
for required_job in FORMAT_TESTS_RESULT QUALITY_CHECKS_RESULT; do
|
||||||
|
result="${!required_job:-missing}"
|
||||||
|
if [[ "$result" != "success" ]]; then
|
||||||
|
echo "${required_job}=${result}" >&2
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
exit "$failed"
|
||||||
|
|
||||||
|
e2e-targeted:
|
||||||
|
if: >
|
||||||
|
github.event_name == 'workflow_dispatch' &&
|
||||||
|
(inputs.mode == 'targeted' || inputs.mode == 'targeted-then-full')
|
||||||
|
needs: build-and-unit
|
||||||
|
name: E2E-targeted-${{ matrix.project }}
|
||||||
|
# Use GitHub-hosted runners to avoid self-hosted desktop contention and sleep/power events.
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 35
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
project: ${{ fromJSON(inputs.target_projects || '["chromium-desktop"]') }}
|
||||||
|
env:
|
||||||
|
MATRIX_PROJECT: ${{ matrix.project }}
|
||||||
|
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-targeted-${{ matrix.project }}
|
||||||
|
PLAYWRIGHT_REPORTER_MODE: line-html
|
||||||
|
PLAYWRIGHT_WORKERS: 1
|
||||||
|
PLAYWRIGHT_VIDEO_MODE: on-first-retry
|
||||||
|
TARGET_GREP: ${{ inputs.target_grep }}
|
||||||
|
TARGET_SPECS: ${{ inputs.target_specs }}
|
||||||
|
RUN_ID: ${{ github.run_id }}
|
||||||
|
steps:
|
||||||
|
- name: Normalize workspace permissions
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||||
|
sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
|
||||||
|
foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)"
|
||||||
|
if [[ -n "$foreign_entry" ]]; then
|
||||||
|
trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB"
|
||||||
|
rm -rf "$trash" 2>/dev/null || true
|
||||||
|
mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Run targeted Playwright specs in container
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
case "$MATRIX_PROJECT" in
|
||||||
|
chromium-mobile) project_offset=1 ;;
|
||||||
|
chromium-desktop) project_offset=2 ;;
|
||||||
|
chromium-tablet) project_offset=3 ;;
|
||||||
|
webkit-mobile) project_offset=31 ;;
|
||||||
|
webkit-desktop) project_offset=32 ;;
|
||||||
|
webkit-tablet) project_offset=33 ;;
|
||||||
|
firefox-mobile) project_offset=61 ;;
|
||||||
|
firefox-desktop) project_offset=62 ;;
|
||||||
|
firefox-tablet) project_offset=63 ;;
|
||||||
|
*) echo "Unsupported Playwright project: $MATRIX_PROJECT" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
port_seed=$((20000 + (RUN_ID % 20000) + project_offset))
|
||||||
|
lock_root="${PLAYWRIGHT_PORT_LOCK_ROOT:-/tmp/pleno-playwright-port-locks}"
|
||||||
|
mkdir -p "$lock_root"
|
||||||
|
chmod 1777 "$lock_root" 2>/dev/null || true
|
||||||
|
find "$lock_root" -mindepth 1 -maxdepth 1 -type d -mmin +360 -exec rmdir {} \; 2>/dev/null || true
|
||||||
|
playwright_port_lock=""
|
||||||
|
playwright_dev_port=""
|
||||||
|
for ((candidate = port_seed; candidate < port_seed + 1000; candidate += 1)); do
|
||||||
|
lock_dir="${lock_root}/${candidate}.lock"
|
||||||
|
if ! mkdir "$lock_dir" 2>/dev/null; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ss -H -ltn "sport = :${candidate}" 2>/dev/null | grep -q .; then
|
||||||
|
rmdir "$lock_dir" || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
playwright_port_lock="$lock_dir"
|
||||||
|
playwright_dev_port="$candidate"
|
||||||
|
break
|
||||||
|
done
|
||||||
|
if [[ -z "$playwright_dev_port" ]]; then
|
||||||
|
echo "Unable to find a free Playwright dev-server port." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
trap 'if [[ -n "${playwright_port_lock:-}" ]]; then rmdir "$playwright_port_lock" 2>/dev/null || true; fi' EXIT
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
docker_cmd=(docker)
|
||||||
|
elif sudo -n docker info >/dev/null 2>&1; then
|
||||||
|
docker_cmd=(sudo docker)
|
||||||
|
else
|
||||||
|
echo "Docker is not available to the runner user, and sudo docker is not available." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p output/playwright
|
||||||
|
scripts/ci/runner-diagnostics.sh "before targeted Playwright ${MATRIX_PROJECT}" -- "${docker_cmd[@]}"
|
||||||
|
SYSTEMD_INHIBIT_REASON="Frontend targeted Playwright ${MATRIX_PROJECT}" \
|
||||||
|
scripts/ci/with-systemd-inhibit.sh "${docker_cmd[@]}" run --rm --ipc=host --network host \
|
||||||
|
--volume "$PWD:/source:ro" \
|
||||||
|
--volume "$PWD/output/playwright:/work/output/playwright" \
|
||||||
|
--workdir /work \
|
||||||
|
--env HOME=/tmp \
|
||||||
|
--env CI="${CI:-}" \
|
||||||
|
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
|
||||||
|
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
|
||||||
|
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
|
||||||
|
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
|
||||||
|
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
|
||||||
|
--env MATRIX_PROJECT="$MATRIX_PROJECT" \
|
||||||
|
--env TARGET_GREP="$TARGET_GREP" \
|
||||||
|
--env TARGET_SPECS="$TARGET_SPECS" \
|
||||||
|
mcr.microsoft.com/playwright:v1.58.2-noble \
|
||||||
|
bash -lc '
|
||||||
|
set -euo pipefail
|
||||||
|
tar --exclude=./output/playwright -C /source -cf - . | tar -C /work -xf -
|
||||||
|
git config --global --add safe.directory /work
|
||||||
|
install_dependencies() {
|
||||||
|
local attempt
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if npm ci --legacy-peer-deps --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$attempt" == "3" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "npm ci failed on attempt ${attempt}; retrying..." >&2
|
||||||
|
sleep 20
|
||||||
|
done
|
||||||
|
}
|
||||||
|
install_dependencies
|
||||||
|
ulimit -n 16384 || true
|
||||||
|
mapfile -t spec_args < <(printf "%s\n" "$TARGET_SPECS" | tr "," "\n" | sed "s/^[[:space:]]*//;s/[[:space:]]*$//;/^$/d")
|
||||||
|
if [[ "${#spec_args[@]}" -eq 0 && -z "${TARGET_GREP:-}" ]]; then
|
||||||
|
echo "Provide at least one spec path or grep pattern." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
for spec_path in "${spec_args[@]}"; do
|
||||||
|
if [[ "$spec_path" == /* || "$spec_path" == *".."* || "$spec_path" != tests/e2e/* ]]; then
|
||||||
|
echo "Targeted spec must stay under tests/e2e: $spec_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$spec_path" ]]; then
|
||||||
|
echo "Targeted spec does not exist: $spec_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
args=("${spec_args[@]}")
|
||||||
|
if [[ -n "${TARGET_GREP:-}" ]]; then
|
||||||
|
args+=(--grep "$TARGET_GREP")
|
||||||
|
fi
|
||||||
|
args+=(--project="$MATRIX_PROJECT")
|
||||||
|
npx playwright test "${args[@]}"
|
||||||
|
'
|
||||||
|
|
||||||
|
- name: Runner diagnostics after Playwright failure
|
||||||
|
if: failure() || cancelled()
|
||||||
|
continue-on-error: true
|
||||||
|
run: scripts/ci/runner-diagnostics.sh "after targeted Playwright ${{ matrix.project }}"
|
||||||
|
|
||||||
|
- name: Upload Playwright report
|
||||||
|
if: failure() || cancelled()
|
||||||
|
continue-on-error: true
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: playwright-report-targeted-${{ matrix.project }}
|
||||||
|
path: |
|
||||||
|
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}-*
|
||||||
|
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}
|
||||||
|
if-no-files-found: ignore
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
e2e-pr:
|
e2e-pr:
|
||||||
if: github.event_name != 'schedule'
|
if: >
|
||||||
needs: build-and-unit
|
always() &&
|
||||||
runs-on: [self-hosted, Linux, X64, default]
|
github.event_name != 'schedule' &&
|
||||||
|
needs.build-and-unit.result == 'success' &&
|
||||||
|
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted') &&
|
||||||
|
(
|
||||||
|
github.event_name != 'workflow_dispatch' ||
|
||||||
|
inputs.mode == 'full' ||
|
||||||
|
needs.e2e-targeted.result == 'success'
|
||||||
|
)
|
||||||
|
needs: [build-and-unit, e2e-targeted]
|
||||||
|
name: E2E-pr-${{ matrix.suite }}-${{ matrix.project }}
|
||||||
|
runs-on: ${{ fromJSON(vars.FRONTEND_CI_E2E_RUNNER || '["self-hosted","Linux","X64","pleno","frontend","docker"]') }}
|
||||||
|
timeout-minutes: 45
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: ${{ fromJSON(vars.FRONTEND_CI_PR_E2E_MAX_PARALLEL || '2') }}
|
||||||
|
matrix:
|
||||||
|
suite: [changed-1-of-2, changed-2-of-2, smoke, pr, ct]
|
||||||
|
project: [chromium-desktop, chromium-mobile]
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-pr-${{ matrix.suite }}-${{ matrix.project }}
|
||||||
|
PLAYWRIGHT_REPORTER_MODE: line-html
|
||||||
|
PLAYWRIGHT_WORKERS: 1
|
||||||
|
PLAYWRIGHT_VIDEO_MODE: on-first-retry
|
||||||
steps:
|
steps:
|
||||||
|
- name: Repair self-hosted workspace permissions
|
||||||
|
if: ${{ contains(vars.FRONTEND_CI_E2E_RUNNER || 'self-hosted', 'self-hosted') }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||||
|
sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
|
||||||
|
foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)"
|
||||||
|
if [[ -n "$foreign_entry" ]]; then
|
||||||
|
trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB"
|
||||||
|
rm -rf "$trash" 2>/dev/null || true
|
||||||
|
mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -79,96 +411,421 @@ jobs:
|
|||||||
EVENT_NAME: ${{ github.event_name }}
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
HEAD_SHA: ${{ github.sha }}
|
HEAD_SHA: ${{ github.sha }}
|
||||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
PUSH_BEFORE_SHA: ${{ github.event.before }}
|
PUSH_BEFORE_SHA: ${{ github.event.before }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
zero_sha="0000000000000000000000000000000000000000"
|
zero_sha="0000000000000000000000000000000000000000"
|
||||||
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_BASE_SHA" ]]; then
|
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_BASE_SHA" ]]; then
|
||||||
base_ref="$PR_BASE_SHA"
|
base_ref="$PR_BASE_SHA"
|
||||||
|
head_ref="$PR_HEAD_SHA"
|
||||||
elif [[ -z "$PUSH_BEFORE_SHA" || "$PUSH_BEFORE_SHA" == "$zero_sha" ]]; then
|
elif [[ -z "$PUSH_BEFORE_SHA" || "$PUSH_BEFORE_SHA" == "$zero_sha" ]]; then
|
||||||
git fetch --no-tags --prune origin "$DEFAULT_BRANCH"
|
git fetch --no-tags --prune origin "$DEFAULT_BRANCH"
|
||||||
base_ref="origin/$DEFAULT_BRANCH"
|
base_ref="origin/$DEFAULT_BRANCH"
|
||||||
|
head_ref="$HEAD_SHA"
|
||||||
else
|
else
|
||||||
base_ref="$PUSH_BEFORE_SHA"
|
base_ref="$PUSH_BEFORE_SHA"
|
||||||
|
head_ref="$HEAD_SHA"
|
||||||
|
fi
|
||||||
|
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_HEAD_SHA" ]]; then
|
||||||
|
head_ref="$PR_HEAD_SHA"
|
||||||
|
else
|
||||||
|
head_ref="$HEAD_SHA"
|
||||||
fi
|
fi
|
||||||
echo "base=$base_ref" >> "$GITHUB_OUTPUT"
|
echo "base=$base_ref" >> "$GITHUB_OUTPUT"
|
||||||
echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT"
|
echo "head=$head_ref" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: npm
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Run Playwright PR suite in container
|
||||||
run: npm ci --legacy-peer-deps
|
shell: bash
|
||||||
|
env:
|
||||||
|
DIFF_BASE_REF: ${{ steps.playwright-diff.outputs.base }}
|
||||||
|
DIFF_HEAD_REF: ${{ steps.playwright-diff.outputs.head }}
|
||||||
|
MATRIX_SUITE: ${{ matrix.suite }}
|
||||||
|
MATRIX_PROJECT: ${{ matrix.project }}
|
||||||
|
RUN_ID: ${{ github.run_id }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
case "$MATRIX_SUITE" in
|
||||||
|
changed-1-of-2) suite_offset=0 ;;
|
||||||
|
changed-2-of-2) suite_offset=10 ;;
|
||||||
|
smoke) suite_offset=20 ;;
|
||||||
|
pr) suite_offset=30 ;;
|
||||||
|
ct) suite_offset=40 ;;
|
||||||
|
*) echo "Unsupported Playwright PR suite: $MATRIX_SUITE" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
case "$MATRIX_PROJECT" in
|
||||||
|
chromium-desktop) project_offset=1 ;;
|
||||||
|
chromium-mobile) project_offset=2 ;;
|
||||||
|
*) echo "Unsupported Playwright PR project: $MATRIX_PROJECT" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
port_seed=$((21000 + (RUN_ID % 20000) + suite_offset + project_offset))
|
||||||
|
lock_root="${PLAYWRIGHT_PORT_LOCK_ROOT:-/tmp/pleno-playwright-port-locks}"
|
||||||
|
mkdir -p "$lock_root"
|
||||||
|
chmod 1777 "$lock_root" 2>/dev/null || true
|
||||||
|
find "$lock_root" -mindepth 1 -maxdepth 1 -type d -mmin +360 -exec rmdir {} \; 2>/dev/null || true
|
||||||
|
playwright_port_lock=""
|
||||||
|
playwright_dev_port=""
|
||||||
|
for ((candidate = port_seed; candidate < port_seed + 1000; candidate += 1)); do
|
||||||
|
lock_dir="${lock_root}/${candidate}.lock"
|
||||||
|
if ! mkdir "$lock_dir" 2>/dev/null; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ss -H -ltn "sport = :${candidate}" 2>/dev/null | grep -q .; then
|
||||||
|
rmdir "$lock_dir" || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
playwright_port_lock="$lock_dir"
|
||||||
|
playwright_dev_port="$candidate"
|
||||||
|
break
|
||||||
|
done
|
||||||
|
if [[ -z "$playwright_dev_port" ]]; then
|
||||||
|
echo "Unable to find a free Playwright dev-server port." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
trap 'if [[ -n "${playwright_port_lock:-}" ]]; then rmdir "$playwright_port_lock" 2>/dev/null || true; fi' EXIT
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
docker_cmd=(docker)
|
||||||
|
elif sudo -n docker info >/dev/null 2>&1; then
|
||||||
|
docker_cmd=(sudo docker)
|
||||||
|
else
|
||||||
|
echo "Docker is not available to the runner user, and sudo docker is not available." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p output/playwright
|
||||||
|
scripts/ci/runner-diagnostics.sh "before Playwright PR ${MATRIX_SUITE}/${MATRIX_PROJECT}" -- "${docker_cmd[@]}"
|
||||||
|
SYSTEMD_INHIBIT_REASON="Frontend Playwright PR ${MATRIX_SUITE}/${MATRIX_PROJECT}" \
|
||||||
|
scripts/ci/with-systemd-inhibit.sh "${docker_cmd[@]}" run --rm --ipc=host --network host \
|
||||||
|
--volume "$PWD:/source:ro" \
|
||||||
|
--volume "$PWD/output/playwright:/work/output/playwright" \
|
||||||
|
--workdir /work \
|
||||||
|
--env HOME=/tmp \
|
||||||
|
--env CI="${CI:-}" \
|
||||||
|
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
|
||||||
|
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
|
||||||
|
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
|
||||||
|
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
|
||||||
|
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
|
||||||
|
--env MATRIX_SUITE="$MATRIX_SUITE" \
|
||||||
|
--env MATRIX_PROJECT="$MATRIX_PROJECT" \
|
||||||
|
--env DIFF_BASE_REF="$DIFF_BASE_REF" \
|
||||||
|
--env DIFF_HEAD_REF="$DIFF_HEAD_REF" \
|
||||||
|
mcr.microsoft.com/playwright:v1.58.2-noble \
|
||||||
|
bash -lc '
|
||||||
|
set -euo pipefail
|
||||||
|
tar --exclude=./output/playwright -C /source -cf - . | tar -C /work -xf -
|
||||||
|
git config --global --add safe.directory /work
|
||||||
|
install_dependencies() {
|
||||||
|
local attempt
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if npm ci --legacy-peer-deps --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$attempt" == "3" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "npm ci failed on attempt ${attempt}; retrying..." >&2
|
||||||
|
sleep 20
|
||||||
|
done
|
||||||
|
}
|
||||||
|
install_dependencies
|
||||||
|
ulimit -n 16384 || true
|
||||||
|
case "$MATRIX_SUITE" in
|
||||||
|
ct)
|
||||||
|
npm run test:ct -- --project="$MATRIX_PROJECT"
|
||||||
|
;;
|
||||||
|
smoke)
|
||||||
|
npx playwright test --grep @smoke --project="$MATRIX_PROJECT"
|
||||||
|
;;
|
||||||
|
pr)
|
||||||
|
npm run test:e2e:pr -- --core-only --project="$MATRIX_PROJECT"
|
||||||
|
;;
|
||||||
|
changed-1-of-2)
|
||||||
|
npm run test:e2e:pr -- --changed-only --project="$MATRIX_PROJECT" --base="$DIFF_BASE_REF" --head="$DIFF_HEAD_REF" -- --shard=1/2 --pass-with-no-tests
|
||||||
|
;;
|
||||||
|
changed-2-of-2)
|
||||||
|
npm run test:e2e:pr -- --changed-only --project="$MATRIX_PROJECT" --base="$DIFF_BASE_REF" --head="$DIFF_HEAD_REF" -- --shard=2/2 --pass-with-no-tests
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported Playwright PR suite: $MATRIX_SUITE" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
'
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
- name: Runner diagnostics after Playwright failure
|
||||||
run: npx playwright install --with-deps chromium
|
if: failure() || cancelled()
|
||||||
|
continue-on-error: true
|
||||||
- name: Run Playwright PR tests
|
run: scripts/ci/runner-diagnostics.sh "after Playwright PR ${{ matrix.suite }}/${{ matrix.project }}"
|
||||||
run: npm run test:e2e:pr -- --base="${{ steps.playwright-diff.outputs.base }}" --head="${{ steps.playwright-diff.outputs.head }}"
|
|
||||||
|
|
||||||
- name: Upload Playwright report
|
- name: Upload Playwright report
|
||||||
if: failure()
|
if: failure() || cancelled()
|
||||||
uses: actions/upload-artifact@v4
|
continue-on-error: true
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: playwright-report-pr
|
name: playwright-report-pr-${{ matrix.suite }}-${{ matrix.project }}
|
||||||
path: output/playwright
|
path: |
|
||||||
|
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}-*
|
||||||
|
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
retention-days: 7
|
retention-days: 1
|
||||||
|
|
||||||
|
required-ci:
|
||||||
|
if: ${{ always() && (github.event_name == 'pull_request' || github.event_name == 'push') }}
|
||||||
|
name: Required CI
|
||||||
|
needs: [format-tests, build-and-unit, e2e-pr]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Verify required jobs succeeded
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
FORMAT_TESTS_RESULT: ${{ needs.format-tests.result }}
|
||||||
|
BUILD_AND_UNIT_RESULT: ${{ needs.build-and-unit.result }}
|
||||||
|
E2E_PR_RESULT: ${{ needs.e2e-pr.result }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
failed=0
|
||||||
|
for required_job in FORMAT_TESTS_RESULT BUILD_AND_UNIT_RESULT E2E_PR_RESULT; do
|
||||||
|
result="${!required_job:-missing}"
|
||||||
|
if [[ "$result" != "success" ]]; then
|
||||||
|
echo "${required_job}=${result}" >&2
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
exit "$failed"
|
||||||
|
|
||||||
e2e-full:
|
e2e-full:
|
||||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch
|
if: >
|
||||||
needs: build-and-unit
|
always() &&
|
||||||
name: E2E-full-${{ matrix.role }}-${{ matrix.browser_label }}-${{ matrix.device }}
|
(
|
||||||
runs-on: [self-hosted, Linux, X64, default]
|
github.event_name == 'schedule' ||
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(
|
||||||
|
github.ref_name == github.event.repository.default_branch &&
|
||||||
|
!(github.event_name == 'push' && github.event.before == 'd393c8c17508c46c61e97bd834a2e407367c69eb')
|
||||||
|
)
|
||||||
|
) &&
|
||||||
|
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted') &&
|
||||||
|
needs.build-and-unit.result == 'success' &&
|
||||||
|
(
|
||||||
|
github.event_name != 'workflow_dispatch' ||
|
||||||
|
inputs.mode == 'full' ||
|
||||||
|
needs.e2e-targeted.result == 'success'
|
||||||
|
)
|
||||||
|
needs: [build-and-unit, e2e-targeted]
|
||||||
|
name: E2E-full-${{ matrix.browser_label }}-${{ matrix.device }}-${{ matrix.role }}-shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}
|
||||||
|
runs-on: ${{ fromJSON(vars.FRONTEND_CI_E2E_RUNNER || '["self-hosted","Linux","X64","pleno","frontend","docker"]') }}
|
||||||
|
timeout-minutes: 60
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
max-parallel: ${{ fromJSON(vars.FRONTEND_CI_FULL_E2E_MAX_PARALLEL || '36') > 0 && github.event_name == 'schedule' && fromJSON(vars.FRONTEND_CI_FULL_E2E_MAX_PARALLEL || '36') || fromJSON(vars.FRONTEND_CI_FULL_E2E_CONCURRENT_MAX_PARALLEL || '26') }}
|
||||||
matrix:
|
matrix:
|
||||||
role: [customer, subuser, admin, superuser]
|
device: [mobile, desktop, tablet]
|
||||||
browser: [chromium, firefox, webkit]
|
role: [superuser, admin, customer, subuser]
|
||||||
device: [mobile, tablet, desktop]
|
shard_index: [1, 2]
|
||||||
|
browser: [chromium, webkit, firefox]
|
||||||
|
exclude:
|
||||||
|
- role: customer
|
||||||
|
shard_index: 2
|
||||||
|
- role: subuser
|
||||||
|
shard_index: 2
|
||||||
include:
|
include:
|
||||||
- browser: chromium
|
- browser: chromium
|
||||||
browser_label: Chromium
|
browser_label: Chromium
|
||||||
browser_install: chromium
|
browser_install: chromium
|
||||||
- browser: firefox
|
|
||||||
browser_label: Firefox
|
|
||||||
browser_install: firefox
|
|
||||||
- browser: webkit
|
- browser: webkit
|
||||||
browser_label: WebKit
|
browser_label: WebKit
|
||||||
browser_install: webkit
|
browser_install: webkit
|
||||||
|
- browser: firefox
|
||||||
|
browser_label: Firefox
|
||||||
|
browser_install: firefox
|
||||||
env:
|
env:
|
||||||
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-full-${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}
|
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-full-${{ matrix.browser }}-${{ matrix.device }}-${{ matrix.role }}-shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}
|
||||||
PLAYWRIGHT_REPORTER_MODE: line-html
|
PLAYWRIGHT_REPORTER_MODE: line-html
|
||||||
|
PLAYWRIGHT_WORKERS: 1
|
||||||
|
PLAYWRIGHT_VIDEO_MODE: off
|
||||||
steps:
|
steps:
|
||||||
|
- name: Repair self-hosted workspace permissions
|
||||||
|
if: ${{ contains(vars.FRONTEND_CI_E2E_RUNNER || 'self-hosted', 'self-hosted') }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ -d "$GITHUB_WORKSPACE" ]]; then
|
||||||
|
sudo -n chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE" 2>/dev/null || true
|
||||||
|
foreign_entry="$(find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 2 ! -user "$(id -u)" -print -quit 2>/dev/null || true)"
|
||||||
|
if [[ -n "$foreign_entry" ]]; then
|
||||||
|
trash="$GITHUB_WORKSPACE/../_workspace-trash-$GITHUB_RUN_ID-$GITHUB_JOB"
|
||||||
|
rm -rf "$trash" 2>/dev/null || true
|
||||||
|
mv "$GITHUB_WORKSPACE" "$trash" 2>/dev/null || true
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: npm
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Run full Playwright slice in container
|
||||||
run: npm ci --legacy-peer-deps
|
shell: bash
|
||||||
|
env:
|
||||||
|
MATRIX_ROLE: ${{ matrix.role }}
|
||||||
|
MATRIX_BROWSER: ${{ matrix.browser }}
|
||||||
|
MATRIX_DEVICE: ${{ matrix.device }}
|
||||||
|
MATRIX_SHARD_INDEX: ${{ matrix.shard_index }}
|
||||||
|
MATRIX_SHARD_TOTAL: ${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}
|
||||||
|
RUN_ID: ${{ github.run_id }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
case "$MATRIX_ROLE" in
|
||||||
|
superuser) role_offset=0 ;;
|
||||||
|
admin) role_offset=100 ;;
|
||||||
|
customer) role_offset=200 ;;
|
||||||
|
subuser) role_offset=300 ;;
|
||||||
|
*) echo "Unsupported Playwright role: $MATRIX_ROLE" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
case "$MATRIX_BROWSER" in
|
||||||
|
chromium) browser_offset=0 ;;
|
||||||
|
webkit) browser_offset=30 ;;
|
||||||
|
firefox) browser_offset=60 ;;
|
||||||
|
*) echo "Unsupported Playwright browser: $MATRIX_BROWSER" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
case "$MATRIX_DEVICE" in
|
||||||
|
mobile) device_offset=1 ;;
|
||||||
|
desktop) device_offset=2 ;;
|
||||||
|
tablet) device_offset=3 ;;
|
||||||
|
*) echo "Unsupported Playwright device: $MATRIX_DEVICE" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
case "$MATRIX_SHARD_INDEX" in
|
||||||
|
1) shard_offset=0 ;;
|
||||||
|
2) shard_offset=400 ;;
|
||||||
|
*) echo "Unsupported Playwright shard index: $MATRIX_SHARD_INDEX" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
port_seed=$((22000 + (RUN_ID % 20000) + role_offset + browser_offset + device_offset + shard_offset))
|
||||||
|
lock_root="${PLAYWRIGHT_PORT_LOCK_ROOT:-/tmp/pleno-playwright-port-locks}"
|
||||||
|
mkdir -p "$lock_root"
|
||||||
|
chmod 1777 "$lock_root" 2>/dev/null || true
|
||||||
|
find "$lock_root" -mindepth 1 -maxdepth 1 -type d -mmin +360 -exec rmdir {} \; 2>/dev/null || true
|
||||||
|
playwright_port_lock=""
|
||||||
|
playwright_dev_port=""
|
||||||
|
for ((candidate = port_seed; candidate < port_seed + 1000; candidate += 1)); do
|
||||||
|
lock_dir="${lock_root}/${candidate}.lock"
|
||||||
|
if ! mkdir "$lock_dir" 2>/dev/null; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ss -H -ltn "sport = :${candidate}" 2>/dev/null | grep -q .; then
|
||||||
|
rmdir "$lock_dir" || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
playwright_port_lock="$lock_dir"
|
||||||
|
playwright_dev_port="$candidate"
|
||||||
|
break
|
||||||
|
done
|
||||||
|
if [[ -z "$playwright_dev_port" ]]; then
|
||||||
|
echo "Unable to find a free Playwright dev-server port." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
trap 'if [[ -n "${playwright_port_lock:-}" ]]; then rmdir "$playwright_port_lock" 2>/dev/null || true; fi' EXIT
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
docker_cmd=(docker)
|
||||||
|
elif sudo -n docker info >/dev/null 2>&1; then
|
||||||
|
docker_cmd=(sudo docker)
|
||||||
|
else
|
||||||
|
echo "Docker is not available to the runner user, and sudo docker is not available." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p output/playwright
|
||||||
|
scripts/ci/runner-diagnostics.sh "before Playwright full ${MATRIX_BROWSER}/${MATRIX_DEVICE}/${MATRIX_ROLE}/shard-${MATRIX_SHARD_INDEX}-of-${MATRIX_SHARD_TOTAL}" -- "${docker_cmd[@]}"
|
||||||
|
SYSTEMD_INHIBIT_REASON="Frontend Playwright full ${MATRIX_BROWSER}/${MATRIX_DEVICE}/${MATRIX_ROLE}/shard-${MATRIX_SHARD_INDEX}-of-${MATRIX_SHARD_TOTAL}" \
|
||||||
|
scripts/ci/with-systemd-inhibit.sh "${docker_cmd[@]}" run --rm --ipc=host --network host \
|
||||||
|
--volume "$PWD:/source:ro" \
|
||||||
|
--volume "$PWD/output/playwright:/work/output/playwright" \
|
||||||
|
--workdir /work \
|
||||||
|
--env HOME=/tmp \
|
||||||
|
--env CI="${CI:-}" \
|
||||||
|
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
|
||||||
|
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
|
||||||
|
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
|
||||||
|
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
|
||||||
|
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
|
||||||
|
--env MATRIX_ROLE="$MATRIX_ROLE" \
|
||||||
|
--env MATRIX_BROWSER="$MATRIX_BROWSER" \
|
||||||
|
--env MATRIX_DEVICE="$MATRIX_DEVICE" \
|
||||||
|
--env MATRIX_SHARD_INDEX="$MATRIX_SHARD_INDEX" \
|
||||||
|
--env MATRIX_SHARD_TOTAL="$MATRIX_SHARD_TOTAL" \
|
||||||
|
mcr.microsoft.com/playwright:v1.58.2-noble \
|
||||||
|
bash -lc '
|
||||||
|
set -euo pipefail
|
||||||
|
tar --exclude=./output/playwright -C /source -cf - . | tar -C /work -xf -
|
||||||
|
git config --global --add safe.directory /work
|
||||||
|
install_dependencies() {
|
||||||
|
local attempt
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if npm ci --legacy-peer-deps --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$attempt" == "3" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "npm ci failed on attempt ${attempt}; retrying..." >&2
|
||||||
|
sleep 20
|
||||||
|
done
|
||||||
|
}
|
||||||
|
install_dependencies
|
||||||
|
ulimit -n 16384 || true
|
||||||
|
npm run test:e2e:full:slice -- --role="$MATRIX_ROLE" --project="$MATRIX_BROWSER-$MATRIX_DEVICE" --shard="$MATRIX_SHARD_INDEX/$MATRIX_SHARD_TOTAL"
|
||||||
|
'
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
- name: Runner diagnostics after Playwright failure
|
||||||
run: npx playwright install --with-deps ${{ matrix.browser_install }}
|
if: failure() || cancelled()
|
||||||
|
continue-on-error: true
|
||||||
- name: Run full Playwright slice
|
run: scripts/ci/runner-diagnostics.sh "after Playwright full ${{ matrix.browser }}/${{ matrix.device }}/${{ matrix.role }}/shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}"
|
||||||
run: npm run test:e2e:full:slice -- --role="${{ matrix.role }}" --project="${{ matrix.browser }}-${{ matrix.device }}"
|
|
||||||
|
|
||||||
- name: Upload Playwright report
|
- name: Upload Playwright report
|
||||||
if: failure()
|
if: failure() || cancelled()
|
||||||
uses: actions/upload-artifact@v4
|
continue-on-error: true
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: playwright-report-full-${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}
|
name: playwright-report-full-${{ matrix.browser }}-${{ matrix.device }}-${{ matrix.role }}-shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}
|
||||||
path: |
|
path: |
|
||||||
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/report
|
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/report
|
||||||
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/test-results
|
output/playwright/${{ env.PLAYWRIGHT_ARTIFACT_NAMESPACE }}/test-results
|
||||||
output/playwright/test-lists/${{ matrix.role }}-${{ matrix.browser }}-${{ matrix.device }}.txt
|
output/playwright/test-lists/${{ matrix.browser }}-${{ matrix.device }}-${{ matrix.role }}.txt
|
||||||
|
output/playwright/test-lists/${{ matrix.browser }}-${{ matrix.device }}-${{ matrix.role }}-shard-${{ matrix.shard_index }}-of-${{ (matrix.role == 'superuser' || matrix.role == 'admin') && 2 || 1 }}.txt
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
retention-days: 14
|
retention-days: 1
|
||||||
|
|
||||||
|
full-e2e-summary:
|
||||||
|
if: >
|
||||||
|
always() &&
|
||||||
|
(
|
||||||
|
github.event_name == 'schedule' ||
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(
|
||||||
|
github.ref_name == github.event.repository.default_branch &&
|
||||||
|
!(github.event_name == 'push' && github.event.before == 'd393c8c17508c46c61e97bd834a2e407367c69eb')
|
||||||
|
)
|
||||||
|
) &&
|
||||||
|
!(github.event_name == 'workflow_dispatch' && inputs.mode == 'targeted')
|
||||||
|
name: Full E2E summary
|
||||||
|
needs: [e2e-full]
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Verify full E2E succeeded
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
FULL_E2E_RESULT: ${{ needs.e2e-full.result }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ "$FULL_E2E_RESULT" != "success" ]]; then
|
||||||
|
echo "E2E_FULL_RESULT=${FULL_E2E_RESULT:-missing}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|||||||
@@ -8,12 +8,22 @@ pnpm-debug.log*
|
|||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
node_modules.codex-permission-blocked-*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
coverage
|
coverage
|
||||||
*.local
|
*.local
|
||||||
dev-dist
|
dev-dist
|
||||||
|
.playwright-cli/
|
||||||
|
|
||||||
|
# Mobile build and signing outputs
|
||||||
|
/app/build/
|
||||||
|
/app-release*.aab
|
||||||
|
/app-release*.apk
|
||||||
|
/app-release*.idsig
|
||||||
|
*.keystore
|
||||||
|
*.jks
|
||||||
|
|
||||||
/cypress/videos/
|
/cypress/videos/
|
||||||
/cypress/screenshots/
|
/cypress/screenshots/
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"setupCompletedAt": "2026-06-07T11:37:04.444Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
ruby ">= 3.2", "< 3.5"
|
||||||
|
gem "fastlane", "2.237.0"
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
CFPropertyList (3.0.8)
|
||||||
|
abbrev (0.1.2)
|
||||||
|
addressable (2.9.0)
|
||||||
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
|
artifactory (3.0.17)
|
||||||
|
atomos (0.1.3)
|
||||||
|
aws-eventstream (1.4.0)
|
||||||
|
aws-partitions (1.1271.0)
|
||||||
|
aws-sdk-core (3.254.0)
|
||||||
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
|
aws-sigv4 (~> 1.9)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
|
jmespath (~> 1, >= 1.6.1)
|
||||||
|
logger
|
||||||
|
aws-sdk-kms (1.130.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.254.0)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sdk-s3 (1.228.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.254.0)
|
||||||
|
aws-sdk-kms (~> 1)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sigv4 (1.12.1)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
babosa (1.0.4)
|
||||||
|
base64 (0.3.0)
|
||||||
|
benchmark (0.5.0)
|
||||||
|
bigdecimal (4.1.2)
|
||||||
|
claide (1.1.0)
|
||||||
|
colored (1.2)
|
||||||
|
colored2 (3.1.2)
|
||||||
|
commander (4.6.0)
|
||||||
|
highline (~> 2.0.0)
|
||||||
|
csv (3.3.5)
|
||||||
|
declarative (0.0.20)
|
||||||
|
digest-crc (0.7.0)
|
||||||
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
|
domain_name (0.6.20240107)
|
||||||
|
dotenv (2.8.1)
|
||||||
|
emoji_regex (3.2.3)
|
||||||
|
excon (1.6.0)
|
||||||
|
logger
|
||||||
|
faraday (1.10.6)
|
||||||
|
faraday-em_http (~> 1.0)
|
||||||
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
faraday-excon (~> 1.1)
|
||||||
|
faraday-httpclient (~> 1.0)
|
||||||
|
faraday-multipart (~> 1.0)
|
||||||
|
faraday-net_http (~> 1.0)
|
||||||
|
faraday-net_http_persistent (~> 1.0)
|
||||||
|
faraday-patron (~> 1.0)
|
||||||
|
faraday-rack (~> 1.0)
|
||||||
|
faraday-retry (~> 1.0)
|
||||||
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-cookie_jar (0.0.8)
|
||||||
|
faraday (>= 0.8.0)
|
||||||
|
http-cookie (>= 1.0.0)
|
||||||
|
faraday-em_http (1.0.0)
|
||||||
|
faraday-em_synchrony (1.0.1)
|
||||||
|
faraday-excon (1.1.0)
|
||||||
|
faraday-httpclient (1.0.1)
|
||||||
|
faraday-multipart (1.2.0)
|
||||||
|
multipart-post (~> 2.0)
|
||||||
|
faraday-net_http (1.0.2)
|
||||||
|
faraday-net_http_persistent (1.2.0)
|
||||||
|
faraday-patron (1.0.0)
|
||||||
|
faraday-rack (1.0.0)
|
||||||
|
faraday-retry (1.0.4)
|
||||||
|
faraday_middleware (1.2.1)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
fastimage (2.4.1)
|
||||||
|
fastlane (2.237.0)
|
||||||
|
CFPropertyList (>= 2.3, < 5.0.0)
|
||||||
|
abbrev (~> 0.1)
|
||||||
|
addressable (>= 2.9.0, < 3.0.0)
|
||||||
|
artifactory (~> 3.0)
|
||||||
|
aws-sdk-s3 (~> 1.197)
|
||||||
|
babosa (>= 1.0.3, < 2.0.0)
|
||||||
|
base64 (~> 0.2)
|
||||||
|
benchmark (>= 0.1.0)
|
||||||
|
bundler (>= 2.4.0, < 5.0.0)
|
||||||
|
colored (~> 1.2)
|
||||||
|
commander (~> 4.6)
|
||||||
|
csv (~> 3.3)
|
||||||
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
|
excon (>= 0.71.0, < 2.0.0)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
faraday-cookie_jar (~> 0.0.6)
|
||||||
|
faraday_middleware (~> 1.0)
|
||||||
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
|
fastlane-sirp (>= 1.1.0)
|
||||||
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
|
google-cloud-env (>= 1.6.0, < 2.3.0)
|
||||||
|
google-cloud-storage (~> 1.31)
|
||||||
|
highline (~> 2.0)
|
||||||
|
http-cookie (~> 1.0.5)
|
||||||
|
json (< 3.0.0)
|
||||||
|
jwt (>= 2.10.3, < 4)
|
||||||
|
logger (>= 1.6, < 2.0)
|
||||||
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
|
multi_json (~> 1.12)
|
||||||
|
multipart-post (>= 2.0.0, < 3.0.0)
|
||||||
|
mutex_m (~> 0.3)
|
||||||
|
naturally (~> 2.2)
|
||||||
|
nkf (~> 0.2)
|
||||||
|
optparse (>= 0.1.1, < 1.0.0)
|
||||||
|
ostruct (>= 0.1.0)
|
||||||
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
|
rubyzip (>= 2.0.0, < 3.0.0)
|
||||||
|
security (= 0.1.5)
|
||||||
|
simctl (~> 1.6.3)
|
||||||
|
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||||
|
terminal-table (~> 3)
|
||||||
|
tty-screen (>= 0.6.3, < 1.0.0)
|
||||||
|
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||||
|
word_wrap (~> 1.0.0)
|
||||||
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
|
xcpretty (~> 0.4.1)
|
||||||
|
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||||
|
fastlane-sirp (1.1.0)
|
||||||
|
gh_inspector (1.1.3)
|
||||||
|
google-apis-androidpublisher_v3 (0.105.0)
|
||||||
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
|
google-apis-core (0.18.0)
|
||||||
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
|
googleauth (~> 1.9)
|
||||||
|
httpclient (>= 2.8.3, < 3.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
mutex_m
|
||||||
|
representable (~> 3.0)
|
||||||
|
retriable (>= 2.0, < 4.a)
|
||||||
|
google-apis-iamcredentials_v1 (0.28.0)
|
||||||
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
|
google-apis-playcustomapp_v1 (0.18.0)
|
||||||
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
|
google-apis-storage_v1 (0.65.0)
|
||||||
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
|
google-cloud-core (1.9.0)
|
||||||
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
|
google-cloud-errors (~> 1.0)
|
||||||
|
google-cloud-env (2.2.2)
|
||||||
|
base64 (~> 0.2)
|
||||||
|
faraday (>= 1.0, < 3.a)
|
||||||
|
google-cloud-errors (1.7.0)
|
||||||
|
google-cloud-storage (1.62.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
digest-crc (~> 0.4)
|
||||||
|
google-apis-core (>= 0.18, < 2)
|
||||||
|
google-apis-iamcredentials_v1 (~> 0.18)
|
||||||
|
google-apis-storage_v1 (>= 0.42)
|
||||||
|
google-cloud-core (~> 1.6)
|
||||||
|
googleauth (~> 1.9)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
google-logging-utils (0.2.0)
|
||||||
|
googleauth (1.17.1)
|
||||||
|
faraday (>= 1.0, < 3.a)
|
||||||
|
google-cloud-env (~> 2.2)
|
||||||
|
google-logging-utils (~> 0.1)
|
||||||
|
jwt (>= 1.4, < 4.0)
|
||||||
|
os (>= 0.9, < 2.0)
|
||||||
|
pstore (~> 0.1)
|
||||||
|
signet (>= 0.16, < 2.a)
|
||||||
|
highline (2.0.3)
|
||||||
|
http-cookie (1.0.8)
|
||||||
|
domain_name (~> 0.5)
|
||||||
|
httpclient (2.9.0)
|
||||||
|
mutex_m
|
||||||
|
jmespath (1.6.2)
|
||||||
|
json (2.21.1)
|
||||||
|
jwt (3.2.0)
|
||||||
|
base64
|
||||||
|
logger (1.7.0)
|
||||||
|
mini_magick (4.13.2)
|
||||||
|
mini_mime (1.1.5)
|
||||||
|
multi_json (1.21.1)
|
||||||
|
multipart-post (2.4.1)
|
||||||
|
mutex_m (0.3.0)
|
||||||
|
nanaimo (0.4.0)
|
||||||
|
naturally (2.3.0)
|
||||||
|
nkf (0.3.0)
|
||||||
|
optparse (0.8.1)
|
||||||
|
os (1.1.4)
|
||||||
|
ostruct (0.6.3)
|
||||||
|
plist (3.7.2)
|
||||||
|
pstore (0.2.1)
|
||||||
|
public_suffix (7.0.5)
|
||||||
|
rake (13.4.2)
|
||||||
|
representable (3.2.0)
|
||||||
|
declarative (< 0.1.0)
|
||||||
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
|
uber (< 0.2.0)
|
||||||
|
retriable (3.8.0)
|
||||||
|
rexml (3.4.4)
|
||||||
|
rouge (3.28.0)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
|
rubyzip (2.4.1)
|
||||||
|
security (0.1.5)
|
||||||
|
signet (0.22.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
faraday (>= 0.17.5, < 3.a)
|
||||||
|
jwt (>= 1.5, < 4.0)
|
||||||
|
simctl (1.6.10)
|
||||||
|
CFPropertyList
|
||||||
|
naturally
|
||||||
|
terminal-notifier (2.0.0)
|
||||||
|
terminal-table (3.0.2)
|
||||||
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
|
trailblazer-option (0.1.2)
|
||||||
|
tty-cursor (0.7.1)
|
||||||
|
tty-screen (0.8.2)
|
||||||
|
tty-spinner (0.9.3)
|
||||||
|
tty-cursor (~> 0.7)
|
||||||
|
uber (0.1.0)
|
||||||
|
unicode-display_width (2.6.0)
|
||||||
|
word_wrap (1.0.0)
|
||||||
|
xcodeproj (1.28.1)
|
||||||
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
|
atomos (~> 0.1.3)
|
||||||
|
base64
|
||||||
|
claide (>= 1.0.2, < 2.0)
|
||||||
|
colored2 (~> 3.1)
|
||||||
|
nanaimo (~> 0.4.0)
|
||||||
|
nkf
|
||||||
|
rexml (>= 3.3.6, < 4.0)
|
||||||
|
xcpretty (0.4.1)
|
||||||
|
rouge (~> 3.28.0)
|
||||||
|
xcpretty-travis-formatter (1.0.1)
|
||||||
|
xcpretty (~> 0.2, >= 0.0.7)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
ruby
|
||||||
|
x86_64-linux
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
fastlane (= 2.237.0)
|
||||||
|
|
||||||
|
RUBY VERSION
|
||||||
|
ruby 3.3.12p206
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.5.22
|
||||||
@@ -16,6 +16,16 @@ See [Vite Configuration Reference](https://vite.dev/config/).
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Contributing Changes
|
||||||
|
|
||||||
|
Create a scoped feature branch, push it, and open a pull request targeting
|
||||||
|
`master`. Do not push directly to `master`. Merge only after the `Required CI`
|
||||||
|
check succeeds, all review conversations are resolved, and the branch is up to
|
||||||
|
date. Use squash merge so `master` retains linear history.
|
||||||
|
|
||||||
|
See [`.github/BRANCH_PROTECTION.md`](.github/BRANCH_PROTECTION.md) for the
|
||||||
|
repository policy, rollout checks, and emergency bypass procedure.
|
||||||
|
|
||||||
### Compile and Hot-Reload for Development
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -96,16 +106,22 @@ Artifacts and summaries:
|
|||||||
|
|
||||||
## Playwright Full E2E
|
## Playwright Full E2E
|
||||||
|
|
||||||
Run the permanent grouped full-suite entrypoint with a hard max of 5 total workers across:
|
Run the permanent grouped full-suite entrypoint with a hard max of 5 total workers across the browser-engine groups:
|
||||||
|
|
||||||
- `chromium-(desktop|tablet|mobile)`
|
- Chromium
|
||||||
- `firefox-(desktop|tablet|mobile)`
|
- WebKit
|
||||||
- `webkit-(desktop|tablet|mobile)`
|
- Firefox
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run test:e2e:ci
|
npm run test:e2e:ci
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The full CI matrix is ordered by browser engine, then device class, then user role:
|
||||||
|
|
||||||
|
- browsers: `chromium`, `webkit`, `firefox`
|
||||||
|
- devices: `mobile`, `desktop`, `tablet`
|
||||||
|
- roles: `superuser`, `admin`, `customer`, `subuser`
|
||||||
|
|
||||||
Run a single full-suite slice for one role and one Playwright project:
|
Run a single full-suite slice for one role and one Playwright project:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -117,7 +133,7 @@ Default worker allocation:
|
|||||||
```sh
|
```sh
|
||||||
PLAYWRIGHT_PARALLEL_WORKERS_CHROMIUM=2
|
PLAYWRIGHT_PARALLEL_WORKERS_CHROMIUM=2
|
||||||
PLAYWRIGHT_PARALLEL_WORKERS_FIREFOX=1
|
PLAYWRIGHT_PARALLEL_WORKERS_FIREFOX=1
|
||||||
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=2
|
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=1
|
||||||
```
|
```
|
||||||
|
|
||||||
Optional overrides:
|
Optional overrides:
|
||||||
@@ -126,18 +142,70 @@ Optional overrides:
|
|||||||
PLAYWRIGHT_PARALLEL_BASE_PORT=5191
|
PLAYWRIGHT_PARALLEL_BASE_PORT=5191
|
||||||
PLAYWRIGHT_PARALLEL_WORKERS_CHROMIUM=2
|
PLAYWRIGHT_PARALLEL_WORKERS_CHROMIUM=2
|
||||||
PLAYWRIGHT_PARALLEL_WORKERS_FIREFOX=1
|
PLAYWRIGHT_PARALLEL_WORKERS_FIREFOX=1
|
||||||
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=2
|
PLAYWRIGHT_PARALLEL_WORKERS_WEBKIT=1
|
||||||
```
|
```
|
||||||
|
|
||||||
The runner fails fast if the combined worker count exceeds 5.
|
The runner fails fast if the combined worker count exceeds 5.
|
||||||
|
|
||||||
|
GitHub Actions keeps the full browser/device/role matrix stable by capping full-suite
|
||||||
|
matrix parallelism at 2 jobs, running each full slice with `PLAYWRIGHT_WORKERS=1`,
|
||||||
|
wrapping Docker Playwright runs with `systemd-inhibit` when available, and setting
|
||||||
|
`PLAYWRIGHT_VIDEO_MODE=off` for the full matrix. The E2E network harness serves
|
||||||
|
Font Awesome from local fixtures so WebKit page loads and visual snapshots do not
|
||||||
|
depend on CDN/TLS availability. Traces and screenshots are still retained on
|
||||||
|
failure.
|
||||||
|
|
||||||
Artifacts and summaries:
|
Artifacts and summaries:
|
||||||
|
|
||||||
- `output/playwright/ci-parallel-report/index.html`
|
- `output/playwright/ci-parallel-report/index.html`
|
||||||
- `output/playwright/ci-parallel-chromium/report/index.html`
|
- `output/playwright/ci-parallel-chromium/report/index.html`
|
||||||
- `output/playwright/ci-parallel-firefox/report/index.html`
|
- `output/playwright/ci-parallel-firefox/report/index.html`
|
||||||
- `output/playwright/ci-parallel-webkit/report/index.html`
|
- `output/playwright/ci-parallel-webkit/report/index.html`
|
||||||
- `output/playwright/test-lists/<role>-<project>.txt`
|
- `output/playwright/test-lists/<project>-<role>.txt`
|
||||||
|
- `output/playwright/test-lists/<role>-<project>.txt` (legacy compatibility copy)
|
||||||
|
|
||||||
|
## Android App Icon
|
||||||
|
|
||||||
|
The Play Store Android package is built from the Capacitor project in `android/`.
|
||||||
|
The legacy Bubblewrap/TWA project at the repository root is not used by
|
||||||
|
`npm run mobile:android:bundle`.
|
||||||
|
|
||||||
|
The native launcher and store icons use the opaque iOS marketing icon as their
|
||||||
|
shared master so Android and iOS keep the same white background:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Regenerate the checked-in launcher assets after changing that source image:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run mobile:android:icons
|
||||||
|
```
|
||||||
|
|
||||||
|
Check that the generated Android launcher assets are current:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run mobile:android:icons:check
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm run mobile:android:sync` runs the icon generator before building and syncing
|
||||||
|
the Capacitor Android project. The generator updates `android/app/src/main/res`
|
||||||
|
launcher assets, `public/icons/icon-192x192.png`, `public/icons/icon-512x512.png`,
|
||||||
|
and `store_icon.png`.
|
||||||
|
|
||||||
|
## Mobile Store Releases
|
||||||
|
|
||||||
|
Signed Android and iOS store artifacts are built through the GitHub Actions
|
||||||
|
`Mobile Store Artifacts` workflow. By default, current `master` after green
|
||||||
|
`Automated Tests` uploads Android to Google Play production and uploads iOS to
|
||||||
|
App Store Connect.
|
||||||
|
|
||||||
|
See `docs/mobile-artifacts.md` for workflow triggers, required secrets, and
|
||||||
|
local mobile checks. See `docs/app-store-release.md` for App Store Connect
|
||||||
|
release preparation and review notes. For a separate development-signed IPA
|
||||||
|
that can be installed on an approved iPhone from Ubuntu over USB, see
|
||||||
|
`docs/ios-device-debug.md`.
|
||||||
|
|
||||||
## Bubblewrap (TWA) Build and Install
|
## Bubblewrap (TWA) Build and Install
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||||
|
|
||||||
|
# Built application files
|
||||||
|
*.apk
|
||||||
|
*.aar
|
||||||
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# Files for the ART/Dalvik VM
|
||||||
|
*.dex
|
||||||
|
|
||||||
|
# Java class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
bin/
|
||||||
|
gen/
|
||||||
|
out/
|
||||||
|
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||||
|
# release/
|
||||||
|
|
||||||
|
# Gradle files
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Proguard folder generated by Eclipse
|
||||||
|
proguard/
|
||||||
|
|
||||||
|
# Log Files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Android Studio Navigation editor temp files
|
||||||
|
.navigation/
|
||||||
|
|
||||||
|
# Android Studio captures folder
|
||||||
|
captures/
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
*.iml
|
||||||
|
.idea/workspace.xml
|
||||||
|
.idea/tasks.xml
|
||||||
|
.idea/gradle.xml
|
||||||
|
.idea/assetWizardSettings.xml
|
||||||
|
.idea/dictionaries
|
||||||
|
.idea/libraries
|
||||||
|
# Android Studio 3 in .gitignore file.
|
||||||
|
.idea/caches
|
||||||
|
.idea/modules.xml
|
||||||
|
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||||
|
.idea/navEditor.xml
|
||||||
|
|
||||||
|
# Keystore files
|
||||||
|
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||||
|
#*.jks
|
||||||
|
#*.keystore
|
||||||
|
|
||||||
|
# External native build folder generated in Android Studio 2.2 and later
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Google Services (e.g. APIs or Firebase)
|
||||||
|
# google-services.json
|
||||||
|
|
||||||
|
# Freeline
|
||||||
|
freeline.py
|
||||||
|
freeline/
|
||||||
|
freeline_project_description.json
|
||||||
|
|
||||||
|
# fastlane
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/Preview.html
|
||||||
|
fastlane/screenshots
|
||||||
|
fastlane/test_output
|
||||||
|
fastlane/readme.md
|
||||||
|
|
||||||
|
# Version control
|
||||||
|
vcs.xml
|
||||||
|
|
||||||
|
# lint
|
||||||
|
lint/intermediates/
|
||||||
|
lint/generated/
|
||||||
|
lint/outputs/
|
||||||
|
lint/tmp/
|
||||||
|
# lint/reports/
|
||||||
|
|
||||||
|
# Android Profiling
|
||||||
|
*.hprof
|
||||||
|
|
||||||
|
# Cordova plugins for Capacitor
|
||||||
|
capacitor-cordova-android-plugins
|
||||||
|
|
||||||
|
# Copied web assets
|
||||||
|
app/src/main/assets/public
|
||||||
|
|
||||||
|
# Generated Config files
|
||||||
|
app/src/main/assets/capacitor.config.json
|
||||||
|
app/src/main/assets/capacitor.plugins.json
|
||||||
|
app/src/main/res/xml/config.xml
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/build/*
|
||||||
|
!/build/.npmkeep
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
def mobileEnv = { String name, String defaultValue = null ->
|
||||||
|
return System.getenv(name) ?: (project.hasProperty(name) ? project.property(name).toString() : defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
def releaseStoreFile = mobileEnv('ANDROID_KEYSTORE_FILE')
|
||||||
|
def hasReleaseSigning = releaseStoreFile &&
|
||||||
|
mobileEnv('ANDROID_KEYSTORE_PASSWORD') &&
|
||||||
|
mobileEnv('ANDROID_KEY_ALIAS') &&
|
||||||
|
mobileEnv('ANDROID_KEY_PASSWORD')
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.truckwash.twa"
|
||||||
|
compileSdk = rootProject.ext.compileSdkVersion
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "io.truckwash.twa"
|
||||||
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
|
versionCode Integer.parseInt(mobileEnv('MOBILE_VERSION_CODE', '1'))
|
||||||
|
versionName mobileEnv('MOBILE_VERSION_NAME', '1.0')
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
aaptOptions {
|
||||||
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||||
|
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
if (hasReleaseSigning) {
|
||||||
|
storeFile file(releaseStoreFile)
|
||||||
|
storePassword mobileEnv('ANDROID_KEYSTORE_PASSWORD')
|
||||||
|
keyAlias mobileEnv('ANDROID_KEY_ALIAS')
|
||||||
|
keyPassword mobileEnv('ANDROID_KEY_PASSWORD')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
if (hasReleaseSigning) {
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
flatDir{
|
||||||
|
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||||
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||||
|
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||||
|
implementation project(':capacitor-android')
|
||||||
|
testImplementation "junit:junit:$junitVersion"
|
||||||
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||||
|
implementation project(':capacitor-cordova-android-plugins')
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: 'capacitor.build.gradle'
|
||||||
|
|
||||||
|
try {
|
||||||
|
def servicesJSON = file('google-services.json')
|
||||||
|
if (servicesJSON.text) {
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
|
dependencies {
|
||||||
|
implementation project(':capacitor-geolocation')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (hasProperty('postBuildExtras')) {
|
||||||
|
postBuildExtras()
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package io.truckwash.twa;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ExampleInstrumentedTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void useAppContext() throws Exception {
|
||||||
|
// Context of the app under test.
|
||||||
|
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||||
|
|
||||||
|
assertEquals("io.truckwash.twa", appContext.getPackageName());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/title_activity_main"
|
||||||
|
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths"></meta-data>
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera.autofocus"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.location.gps"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
|
<!-- Permissions -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package io.truckwash.twa;
|
||||||
|
|
||||||
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
|
||||||
|
public class MainActivity extends BridgeActivity {}
|
||||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="78.5885"
|
||||||
|
android:endY="90.9159"
|
||||||
|
android:startX="48.7653"
|
||||||
|
android:startY="61.0927"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#26A69A"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
</vector>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Truck Wash</string>
|
||||||
|
<string name="title_activity_main">Truck Wash</string>
|
||||||
|
<string name="package_name">io.truckwash.twa</string>
|
||||||
|
<string name="custom_url_scheme">io.truckwash.twa</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
<item name="android:background">@null</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
|
<item name="android:background">@drawable/splash</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-path name="my_images" path="." />
|
||||||
|
<cache-path name="my_cache_images" path="." />
|
||||||
|
</paths>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package io.truckwash.twa;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
public class ExampleUnitTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addition_isCorrect() throws Exception {
|
||||||
|
assertEquals(4, 2 + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:8.13.0'
|
||||||
|
classpath 'com.google.gms:google-services:4.4.4'
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "variables.gradle"
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
include ':capacitor-android'
|
||||||
|
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||||
|
|
||||||
|
include ':capacitor-geolocation'
|
||||||
|
project(':capacitor-geolocation').projectDir = new File('../node_modules/@capacitor/geolocation/android')
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH="\\\"\\\""
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
include ':app'
|
||||||
|
include ':capacitor-cordova-android-plugins'
|
||||||
|
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||||
|
|
||||||
|
apply from: 'capacitor.settings.gradle'
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
ext {
|
||||||
|
minSdkVersion = 24
|
||||||
|
compileSdkVersion = 36
|
||||||
|
targetSdkVersion = 36
|
||||||
|
androidxActivityVersion = '1.11.0'
|
||||||
|
androidxAppCompatVersion = '1.7.1'
|
||||||
|
androidxCoordinatorLayoutVersion = '1.3.0'
|
||||||
|
androidxCoreVersion = '1.17.0'
|
||||||
|
androidxFragmentVersion = '1.8.9'
|
||||||
|
coreSplashScreenVersion = '1.2.0'
|
||||||
|
androidxWebkitVersion = '1.14.0'
|
||||||
|
junitVersion = '4.13.2'
|
||||||
|
androidxJunitVersion = '1.3.0'
|
||||||
|
androidxEspressoCoreVersion = '3.7.0'
|
||||||
|
cordovaAndroidVersion = '14.0.1'
|
||||||
|
}
|
||||||
@@ -56,9 +56,9 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "io.truckwash.twa"
|
applicationId "io.truckwash.twa"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 35
|
targetSdkVersion 36
|
||||||
versionCode 6
|
versionCode 7
|
||||||
versionName "6"
|
versionName "7"
|
||||||
|
|
||||||
// The name for the application
|
// The name for the application
|
||||||
resValue "string", "appName", twaManifest.name
|
resValue "string", "appName", twaManifest.name
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<!-- Automatically generated file. DO NOT MODIFY -->
|
|
||||||
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<bool name="enableNotification">true</bool>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<bool name="enableSiteSettingsShortcut">true</bool>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<color name="backgroundColor">#0787BB</color>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<color name="colorPrimary">#063651</color>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<color name="colorPrimaryDark">#000000</color>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<color name="navigationColor">#000000</color>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<color name="navigationColorDark">#000000</color>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<color name="navigationDividerColor">#000000</color>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<color name="navigationDividerColorDark">#000000</color>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<integer name="splashScreenFadeOutDuration">300</integer>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<string name="appName" translatable="false">Truck Wash</string>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<string name="fallbackType" translatable="false">customtabs</string>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<string name="fullScopeUrl" translatable="false">https://truckwash.io/</string>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<string name="generatorApp" translatable="false">bubblewrap-cli</string>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<string name="hostName" translatable="false">truckwash.io</string>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<string name="launchHandlerClientMode" translatable="false">navigate-existing</string>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<string name="launchUrl" translatable="false">https://truckwash.io/</string>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<string name="launcherName" translatable="false">Truck Wash</string>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<string name="orientation" translatable="false">portrait</string>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<string name="providerAuthority" translatable="false">io.truckwash.twa.fileprovider</string>
|
|
||||||
<!-- Value from default config. -->
|
|
||||||
<string name="webManifestUrl" translatable="false">https://truckwash.io/manifest.webmanifest</string>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
-keep class androidx.core.app.CoreComponentFactory { <init>(); }
|
|
||||||
-keep class androidx.core.content.FileProvider { <init>(); }
|
|
||||||
-keep class androidx.profileinstaller.ProfileInstallReceiver { <init>(); }
|
|
||||||
-keep class androidx.startup.InitializationProvider { <init>(); }
|
|
||||||
-keep class com.google.android.gms.common.api.GoogleApiActivity { <init>(); }
|
|
||||||
-keep class com.google.androidbrowserhelper.locationdelegation.PermissionRequestActivity { <init>(); }
|
|
||||||
-keep class com.google.androidbrowserhelper.trusted.FocusActivity { <init>(); }
|
|
||||||
-keep class com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity { <init>(); }
|
|
||||||
-keep class com.google.androidbrowserhelper.trusted.NotificationPermissionRequestActivity { <init>(); }
|
|
||||||
-keep class com.google.androidbrowserhelper.trusted.WebViewFallbackActivity { <init>(); }
|
|
||||||
-keep class io.truckwash.twa.Application { <init>(); }
|
|
||||||
-keep class io.truckwash.twa.DelegationService { <init>(); }
|
|
||||||
-keep class io.truckwash.twa.LauncherActivity { <init>(); }
|
|
||||||
-keep class android.widget.Space { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.app.AlertController$RecycleListView { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.view.menu.ActionMenuItemView { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.view.menu.ExpandedMenuView { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.view.menu.ListMenuItemView { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.ActionBarContainer { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.ActionBarContextView { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.ActionBarOverlayLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.ActionMenuView { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.ActivityChooserView$InnerLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.AlertDialogLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.ButtonBarLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.ContentFrameLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.DialogTitle { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.FitWindowsFrameLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.FitWindowsLinearLayout { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.SearchView$SearchAutoComplete { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.Toolbar { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.appcompat.widget.ViewStubCompat { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.browser.browseractions.BrowserActionsFallbackMenuView { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
-keep class androidx.core.widget.NestedScrollView { <init>(android.content.Context, android.util.AttributeSet); }
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#- File Locator -
|
|
||||||
listingFile=../../../../outputs/apk/release/output-metadata.json
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
appMetadataVersion=1.1
|
|
||||||
androidGradlePluginVersion=8.9.1
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 3,
|
|
||||||
"artifactType": {
|
|
||||||
"type": "BUNDLE",
|
|
||||||
"kind": "RegularFile"
|
|
||||||
},
|
|
||||||
"applicationId": "io.truckwash.twa",
|
|
||||||
"variantName": "release",
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"type": "SINGLE",
|
|
||||||
"filters": [],
|
|
||||||
"attributes": [],
|
|
||||||
"outputFile": "../../../../outputs/bundle/release/app-release.aab"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"elementType": "File"
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#- File Locator -
|
|
||||||
listingFile=../../../bundle_ide_model/release/produceReleaseBundleIdeListingFile/output-metadata.json
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
Copyright 2019 Google Inc. All Rights Reserved.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
<!--
|
|
||||||
The "package" attribute is rewritten by the Gradle build with the value of applicationId.
|
|
||||||
It is still required here, as it is used to derive paths, for instance when referring
|
|
||||||
to an Activity by ".MyActivity" instead of the full name. If more Activities are added to the
|
|
||||||
application, the package attribute will need to reflect the correct path in order to use
|
|
||||||
the abbreviated format.
|
|
||||||
-->
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="io.truckwash.twa"
|
|
||||||
android:versionCode="6"
|
|
||||||
android:versionName="6" >
|
|
||||||
|
|
||||||
<uses-sdk
|
|
||||||
android:minSdkVersion="21"
|
|
||||||
android:targetSdkVersion="35" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
||||||
|
|
||||||
<queries>
|
|
||||||
<intent>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="https" />
|
|
||||||
</intent>
|
|
||||||
</queries>
|
|
||||||
|
|
||||||
<permission
|
|
||||||
android:name="io.truckwash.twa.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
|
||||||
android:protectionLevel="signature" />
|
|
||||||
|
|
||||||
<uses-permission android:name="io.truckwash.twa.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:name="io.truckwash.twa.Application"
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:appComponentFactory="androidx.core.app.CoreComponentFactory"
|
|
||||||
android:extractNativeLibs="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/appName"
|
|
||||||
android:manageSpaceActivity="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" >
|
|
||||||
<meta-data
|
|
||||||
android:name="asset_statements"
|
|
||||||
android:resource="@string/assetStatements" />
|
|
||||||
<meta-data
|
|
||||||
android:name="web_manifest_url"
|
|
||||||
android:value="@string/webManifestUrl" />
|
|
||||||
<meta-data
|
|
||||||
android:name="twa_generator"
|
|
||||||
android:value="@string/generatorApp" />
|
|
||||||
|
|
||||||
<activity android:name="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity" >
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.MANAGE_SPACE_URL"
|
|
||||||
android:value="@string/launchUrl" />
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name="io.truckwash.twa.LauncherActivity"
|
|
||||||
android:alwaysRetainTaskState="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/launcherName" >
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.DEFAULT_URL"
|
|
||||||
android:value="@string/launchUrl" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR"
|
|
||||||
android:resource="@color/colorPrimary" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.LAUNCH_HANDLER_CLIENT_MODE"
|
|
||||||
android:value="@string/launchHandlerClientMode" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR_DARK"
|
|
||||||
android:resource="@color/colorPrimaryDark" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR"
|
|
||||||
android:resource="@color/navigationColor" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR_DARK"
|
|
||||||
android:resource="@color/navigationColorDark" />
|
|
||||||
<meta-data
|
|
||||||
android:name="androix.browser.trusted.NAVIGATION_BAR_DIVIDER_COLOR"
|
|
||||||
android:resource="@color/navigationDividerColor" />
|
|
||||||
<meta-data
|
|
||||||
android:name="androix.browser.trusted.NAVIGATION_BAR_DIVIDER_COLOR_DARK"
|
|
||||||
android:resource="@color/navigationDividerColorDark" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.SPLASH_IMAGE_DRAWABLE"
|
|
||||||
android:resource="@drawable/splash" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.SPLASH_SCREEN_BACKGROUND_COLOR"
|
|
||||||
android:resource="@color/backgroundColor" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.SPLASH_SCREEN_FADE_OUT_DURATION"
|
|
||||||
android:value="@integer/splashScreenFadeOutDuration" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.FILE_PROVIDER_AUTHORITY"
|
|
||||||
android:value="@string/providerAuthority" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.app.shortcuts"
|
|
||||||
android:resource="@xml/shortcuts" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.FALLBACK_STRATEGY"
|
|
||||||
android:value="@string/fallbackType" />
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.SCREEN_ORIENTATION"
|
|
||||||
android:value="@string/orientation" />
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter android:autoVerify="true" >
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="@string/hostName"
|
|
||||||
android:scheme="https" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity android:name="com.google.androidbrowserhelper.trusted.FocusActivity" />
|
|
||||||
<activity
|
|
||||||
android:name="com.google.androidbrowserhelper.trusted.WebViewFallbackActivity"
|
|
||||||
android:configChanges="orientation|screenSize" />
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="@string/providerAuthority"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true" >
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/filepaths" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="io.truckwash.twa.DelegationService"
|
|
||||||
android:enabled="@bool/enableNotification"
|
|
||||||
android:exported="@bool/enableNotification" >
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.customtabs.trusted.SMALL_ICON"
|
|
||||||
android:resource="@drawable/ic_notification_icon" />
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<activity android:name="com.google.androidbrowserhelper.trusted.NotificationPermissionRequestActivity" />
|
|
||||||
<activity android:name="com.google.androidbrowserhelper.locationdelegation.PermissionRequestActivity" />
|
|
||||||
<activity
|
|
||||||
android:name="com.google.android.gms.common.api.GoogleApiActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.android.gms.version"
|
|
||||||
android:value="@integer/google_play_services_version" />
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.startup.InitializationProvider"
|
|
||||||
android:authorities="io.truckwash.twa.androidx-startup"
|
|
||||||
android:exported="false" >
|
|
||||||
<meta-data
|
|
||||||
android:name="androidx.emoji2.text.EmojiCompatInitializer"
|
|
||||||
android:value="androidx.startup" />
|
|
||||||
<meta-data
|
|
||||||
android:name="androidx.lifecycle.ProcessLifecycleInitializer"
|
|
||||||
android:value="androidx.startup" />
|
|
||||||
<meta-data
|
|
||||||
android:name="androidx.profileinstaller.ProfileInstallerInitializer"
|
|
||||||
android:value="androidx.startup" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name="androidx.profileinstaller.ProfileInstallReceiver"
|
|
||||||
android:directBootAware="false"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:permission="android.permission.DUMP" >
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="androidx.profileinstaller.action.INSTALL_PROFILE" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="androidx.profileinstaller.action.SKIP_FILE" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="androidx.profileinstaller.action.SAVE_PROFILE" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,873 +0,0 @@
|
|||||||
Landroidx/activity/a;
|
|
||||||
Landroidx/activity/ComponentActivity$4;
|
|
||||||
Landroidx/lifecycle/j;
|
|
||||||
Landroidx/lifecycle/k;
|
|
||||||
HSPLandroidx/activity/ComponentActivity$4;->h(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
|
|
||||||
Landroidx/activity/b;
|
|
||||||
Landroidx/core/app/b;
|
|
||||||
Landroidx/lifecycle/l;
|
|
||||||
Landroidx/lifecycle/E;
|
|
||||||
Landroidx/lifecycle/f;
|
|
||||||
LI/d;
|
|
||||||
Landroidx/activity/c;
|
|
||||||
HSPLandroidx/activity/c;-><init>(Z)V
|
|
||||||
HSPLandroidx/activity/c;->a()V
|
|
||||||
PLandroidx/activity/c;->b(Landroidx/activity/a;)V
|
|
||||||
HSPLandroidx/activity/c;->c(Z)V
|
|
||||||
Landroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;
|
|
||||||
PLandroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;->d()V
|
|
||||||
HSPLandroidx/activity/OnBackPressedDispatcher$LifecycleOnBackPressedCancellable;->h(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
|
|
||||||
Landroidx/activity/OnBackPressedDispatcher;
|
|
||||||
HSPLandroidx/activity/OnBackPressedDispatcher;->a(Landroidx/activity/c;)Landroidx/activity/a;
|
|
||||||
Lc/a;
|
|
||||||
Lc/d;
|
|
||||||
Lc/e;
|
|
||||||
Lc/f;
|
|
||||||
Lc/g;
|
|
||||||
Lc/h;
|
|
||||||
Lc/i;
|
|
||||||
HSPLc/i;-><clinit>()V
|
|
||||||
Ld/a;
|
|
||||||
HSPLd/a;-><init>(II)V
|
|
||||||
Le/a;
|
|
||||||
Lg/b;
|
|
||||||
Lh/a;
|
|
||||||
HSPLh/a;-><init>(Landroid/content/Context;)V
|
|
||||||
HSPLh/a;->a(Landroid/content/Context;)Lh/a;
|
|
||||||
HSPLh/a;->b()I
|
|
||||||
HSPLh/a;->c()I
|
|
||||||
HSPLh/a;->d()Z
|
|
||||||
Lh/b;
|
|
||||||
HSPLh/b;-><clinit>()V
|
|
||||||
HSPLh/b;-><init>(Landroid/content/Context;)V
|
|
||||||
Li/a;
|
|
||||||
Ls/b;
|
|
||||||
HSPLi/a;-><init>(Landroid/content/Context;IIIILjava/lang/CharSequence;)V
|
|
||||||
Landroidx/appcompat/view/menu/a;
|
|
||||||
Landroidx/appcompat/view/menu/h;
|
|
||||||
HSPLandroidx/appcompat/view/menu/a;-><init>(Landroid/content/Context;II)V
|
|
||||||
HSPLandroidx/appcompat/view/menu/a;->k(Landroid/content/Context;Landroidx/appcompat/view/menu/d;)V
|
|
||||||
PLandroidx/appcompat/view/menu/a;->a(Landroidx/appcompat/view/menu/d;Z)V
|
|
||||||
HSPLandroidx/appcompat/view/menu/a;->f(Landroidx/appcompat/view/menu/h$a;)V
|
|
||||||
HSPLandroidx/appcompat/view/menu/a;->j(Z)V
|
|
||||||
Landroidx/appcompat/view/menu/d$a;
|
|
||||||
Landroidx/appcompat/view/menu/d$b;
|
|
||||||
Landroidx/appcompat/view/menu/d;
|
|
||||||
Ls/a;
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;-><clinit>()V
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;-><init>(Landroid/content/Context;)V
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->b(Landroidx/appcompat/view/menu/h;Landroid/content/Context;)V
|
|
||||||
PLandroidx/appcompat/view/menu/d;->close()V
|
|
||||||
PLandroidx/appcompat/view/menu/d;->d(Z)V
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->h(Z)V
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->q()V
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->r()Ljava/util/ArrayList;
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->v()Ljava/util/ArrayList;
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->A()Ljava/util/ArrayList;
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->hasVisibleItems()Z
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->H(Z)V
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->M(Landroidx/appcompat/view/menu/d$a;)V
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->setQwertyMode(Z)V
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->U(Z)V
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->size()I
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->V()V
|
|
||||||
HSPLandroidx/appcompat/view/menu/d;->W()V
|
|
||||||
Landroidx/appcompat/view/menu/h$a;
|
|
||||||
Landroidx/appcompat/view/menu/i;
|
|
||||||
Landroidx/appcompat/widget/a$a;
|
|
||||||
HSPLandroidx/appcompat/widget/a$a;-><init>(Landroidx/appcompat/widget/a;)V
|
|
||||||
Landroidx/appcompat/widget/a;
|
|
||||||
HSPLandroidx/appcompat/widget/a;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
|
||||||
Landroidx/appcompat/widget/b;
|
|
||||||
HSPLandroidx/appcompat/widget/b;-><init>(Landroidx/appcompat/widget/ActionBarContainer;)V
|
|
||||||
HSPLandroidx/appcompat/widget/b;->draw(Landroid/graphics/Canvas;)V
|
|
||||||
HSPLandroidx/appcompat/widget/b;->getOpacity()I
|
|
||||||
HSPLandroidx/appcompat/widget/b;->getOutline(Landroid/graphics/Outline;)V
|
|
||||||
Landroidx/appcompat/widget/ActionBarContainer;
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarContainer;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarContainer;->drawableStateChanged()V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarContainer;->jumpDrawablesToCurrentState()V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarContainer;->onFinishInflate()V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarContainer;->onLayout(ZIIII)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarContainer;->onMeasure(II)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarContainer;->setTabContainer(Landroidx/appcompat/widget/G;)V
|
|
||||||
PLandroidx/appcompat/widget/ActionBarContainer;->verifyDrawable(Landroid/graphics/drawable/Drawable;)Z
|
|
||||||
Landroidx/appcompat/widget/ActionBarContextView;
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarContextView;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarContextView;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
|
||||||
PLandroidx/appcompat/widget/ActionBarContextView;->onDetachedFromWindow()V
|
|
||||||
Landroidx/appcompat/widget/ActionBarOverlayLayout$a;
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$a;-><init>(Landroidx/appcompat/widget/ActionBarOverlayLayout;)V
|
|
||||||
Landroidx/appcompat/widget/ActionBarOverlayLayout$b;
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$b;-><init>(Landroidx/appcompat/widget/ActionBarOverlayLayout;)V
|
|
||||||
Landroidx/appcompat/widget/ActionBarOverlayLayout$c;
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$c;-><init>(Landroidx/appcompat/widget/ActionBarOverlayLayout;)V
|
|
||||||
Landroidx/appcompat/widget/ActionBarOverlayLayout$d;
|
|
||||||
Landroidx/appcompat/widget/ActionBarOverlayLayout$e;
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout$e;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
|
||||||
Landroidx/appcompat/widget/ActionBarOverlayLayout;
|
|
||||||
Landroidx/core/view/o;
|
|
||||||
Landroidx/core/view/p;
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;-><clinit>()V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->h(Landroid/view/View;Landroid/graphics/Rect;ZZZZ)Z
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->generateLayoutParams(Landroid/util/AttributeSet;)Landroid/view/ViewGroup$LayoutParams;
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->k(Landroid/util/AttributeSet;)Landroidx/appcompat/widget/ActionBarOverlayLayout$e;
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->l(Landroid/view/View;)Landroidx/appcompat/widget/u;
|
|
||||||
PLandroidx/appcompat/widget/ActionBarOverlayLayout;->m()V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->n(Landroid/content/Context;)V
|
|
||||||
PLandroidx/appcompat/widget/ActionBarOverlayLayout;->onDetachedFromWindow()V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onLayout(ZIIII)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onMeasure(II)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onStartNestedScroll(Landroid/view/View;Landroid/view/View;I)Z
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->f(Landroid/view/View;Landroid/view/View;II)Z
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->onWindowVisibilityChanged(I)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->q()V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setActionBarVisibilityCallback(Landroidx/appcompat/widget/ActionBarOverlayLayout$d;)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setHasNonEmbeddedTabs(Z)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setWindowCallback(Landroid/view/Window$Callback;)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->setWindowTitle(Ljava/lang/CharSequence;)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionBarOverlayLayout;->shouldDelayChildPressedState()Z
|
|
||||||
Landroidx/appcompat/widget/c$d$a;
|
|
||||||
Landroidx/appcompat/widget/y;
|
|
||||||
HSPLandroidx/appcompat/widget/c$d$a;-><init>(Landroidx/appcompat/widget/c$d;Landroid/view/View;Landroidx/appcompat/widget/c;)V
|
|
||||||
Landroidx/appcompat/widget/c$d;
|
|
||||||
Landroidx/appcompat/widget/l;
|
|
||||||
Landroidx/appcompat/widget/ActionMenuView$a;
|
|
||||||
HSPLandroidx/appcompat/widget/c$d;-><init>(Landroidx/appcompat/widget/c;Landroid/content/Context;)V
|
|
||||||
Landroidx/appcompat/widget/c$f;
|
|
||||||
HSPLandroidx/appcompat/widget/c$f;-><init>(Landroidx/appcompat/widget/c;)V
|
|
||||||
Landroidx/appcompat/widget/c;
|
|
||||||
HSPLandroidx/appcompat/widget/c;-><init>(Landroid/content/Context;)V
|
|
||||||
PLandroidx/appcompat/widget/c;->w()Z
|
|
||||||
HSPLandroidx/appcompat/widget/c;->h()Z
|
|
||||||
PLandroidx/appcompat/widget/c;->z()Z
|
|
||||||
PLandroidx/appcompat/widget/c;->A()Z
|
|
||||||
HSPLandroidx/appcompat/widget/c;->k(Landroid/content/Context;Landroidx/appcompat/view/menu/d;)V
|
|
||||||
PLandroidx/appcompat/widget/c;->a(Landroidx/appcompat/view/menu/d;Z)V
|
|
||||||
HSPLandroidx/appcompat/widget/c;->D(Z)V
|
|
||||||
HSPLandroidx/appcompat/widget/c;->E(Landroidx/appcompat/widget/ActionMenuView;)V
|
|
||||||
HSPLandroidx/appcompat/widget/c;->j(Z)V
|
|
||||||
Landroidx/appcompat/widget/ActionMenuView$e;
|
|
||||||
Landroidx/appcompat/widget/ActionMenuView;
|
|
||||||
Landroidx/appcompat/widget/z;
|
|
||||||
HSPLandroidx/appcompat/widget/ActionMenuView;-><init>(Landroid/content/Context;)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionMenuView;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
|
||||||
PLandroidx/appcompat/widget/ActionMenuView;->w()V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionMenuView;->C(Landroidx/appcompat/view/menu/d;)V
|
|
||||||
PLandroidx/appcompat/widget/ActionMenuView;->onDetachedFromWindow()V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionMenuView;->onLayout(ZIIII)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionMenuView;->onMeasure(II)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionMenuView;->G()Landroidx/appcompat/view/menu/d;
|
|
||||||
HSPLandroidx/appcompat/widget/ActionMenuView;->H(Landroidx/appcompat/view/menu/h$a;Landroidx/appcompat/view/menu/d$a;)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionMenuView;->setOnMenuItemClickListener(Landroidx/appcompat/widget/ActionMenuView$e;)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionMenuView;->setOverflowReserved(Z)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionMenuView;->setPopupTheme(I)V
|
|
||||||
HSPLandroidx/appcompat/widget/ActionMenuView;->setPresenter(Landroidx/appcompat/widget/c;)V
|
|
||||||
Landroidx/appcompat/widget/e;
|
|
||||||
HSPLandroidx/appcompat/widget/e;-><init>(Landroid/view/View;)V
|
|
||||||
HSPLandroidx/appcompat/widget/e;->b()V
|
|
||||||
HSPLandroidx/appcompat/widget/e;->e(Landroid/util/AttributeSet;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/e;->k()Z
|
|
||||||
Landroidx/appcompat/widget/f$a;
|
|
||||||
Landroidx/appcompat/widget/D$f;
|
|
||||||
HSPLandroidx/appcompat/widget/f$a;-><init>()V
|
|
||||||
HSPLandroidx/appcompat/widget/f$a;->f([II)Z
|
|
||||||
HSPLandroidx/appcompat/widget/f$a;->a(Landroidx/appcompat/widget/D;Landroid/content/Context;I)Landroid/graphics/drawable/Drawable;
|
|
||||||
HSPLandroidx/appcompat/widget/f$a;->b(Landroid/content/Context;I)Landroid/content/res/ColorStateList;
|
|
||||||
HSPLandroidx/appcompat/widget/f$a;->d(Landroid/content/Context;ILandroid/graphics/drawable/Drawable;)Z
|
|
||||||
HSPLandroidx/appcompat/widget/f$a;->c(Landroid/content/Context;ILandroid/graphics/drawable/Drawable;)Z
|
|
||||||
Landroidx/appcompat/widget/f;
|
|
||||||
HSPLandroidx/appcompat/widget/f;-><clinit>()V
|
|
||||||
HSPLandroidx/appcompat/widget/f;-><init>()V
|
|
||||||
HSPLandroidx/appcompat/widget/f;->a()Landroid/graphics/PorterDuff$Mode;
|
|
||||||
HSPLandroidx/appcompat/widget/f;->b()Landroidx/appcompat/widget/f;
|
|
||||||
HSPLandroidx/appcompat/widget/f;->e(Landroid/content/Context;I)Landroid/content/res/ColorStateList;
|
|
||||||
HSPLandroidx/appcompat/widget/f;->f()V
|
|
||||||
Landroidx/appcompat/widget/g;
|
|
||||||
HSPLandroidx/appcompat/widget/g;-><init>(Landroid/widget/EditText;)V
|
|
||||||
HSPLandroidx/appcompat/widget/g;->a(Landroid/text/method/KeyListener;)Landroid/text/method/KeyListener;
|
|
||||||
HSPLandroidx/appcompat/widget/g;->c(Landroid/util/AttributeSet;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/g;->e(Z)V
|
|
||||||
Landroidx/appcompat/widget/h;
|
|
||||||
HSPLandroidx/appcompat/widget/h;-><init>(Landroid/widget/TextView;)V
|
|
||||||
HSPLandroidx/appcompat/widget/h;->a([Landroid/text/InputFilter;)[Landroid/text/InputFilter;
|
|
||||||
HSPLandroidx/appcompat/widget/h;->b(Landroid/util/AttributeSet;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/h;->d(Z)V
|
|
||||||
Landroidx/appcompat/widget/j;
|
|
||||||
HSPLandroidx/appcompat/widget/j;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/j;->setBackgroundDrawable(Landroid/graphics/drawable/Drawable;)V
|
|
||||||
HSPLandroidx/appcompat/widget/j;->setImageDrawable(Landroid/graphics/drawable/Drawable;)V
|
|
||||||
Landroidx/appcompat/widget/k;
|
|
||||||
HSPLandroidx/appcompat/widget/k;-><init>(Landroid/widget/ImageView;)V
|
|
||||||
HSPLandroidx/appcompat/widget/k;->b()V
|
|
||||||
HSPLandroidx/appcompat/widget/k;->c()V
|
|
||||||
HSPLandroidx/appcompat/widget/k;->g(Landroid/util/AttributeSet;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/l;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/l;->setBackgroundDrawable(Landroid/graphics/drawable/Drawable;)V
|
|
||||||
HSPLandroidx/appcompat/widget/l;->setImageDrawable(Landroid/graphics/drawable/Drawable;)V
|
|
||||||
Landroidx/appcompat/widget/n;
|
|
||||||
HSPLandroidx/appcompat/widget/n;-><init>(Landroid/widget/TextView;)V
|
|
||||||
Landroidx/appcompat/widget/o$a;
|
|
||||||
Landroidx/core/content/res/h$e;
|
|
||||||
HSPLandroidx/appcompat/widget/o$a;-><init>(Landroidx/appcompat/widget/o;IILjava/lang/ref/WeakReference;)V
|
|
||||||
HSPLandroidx/appcompat/widget/o$a;->f(I)V
|
|
||||||
Landroidx/appcompat/widget/o;
|
|
||||||
HSPLandroidx/appcompat/widget/o;-><init>(Landroid/widget/TextView;)V
|
|
||||||
HSPLandroidx/appcompat/widget/o;->b()V
|
|
||||||
HSPLandroidx/appcompat/widget/o;->d(Landroid/content/Context;Landroidx/appcompat/widget/f;I)Landroidx/appcompat/widget/J;
|
|
||||||
HSPLandroidx/appcompat/widget/o;->m(Landroid/util/AttributeSet;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/o;->o(ZIIII)V
|
|
||||||
HSPLandroidx/appcompat/widget/o;->q(Landroid/content/Context;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/o;->y(Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;)V
|
|
||||||
HSPLandroidx/appcompat/widget/o;->C(Landroid/content/Context;Landroidx/appcompat/widget/L;)V
|
|
||||||
Landroidx/appcompat/widget/p;
|
|
||||||
Landroidx/core/widget/h;
|
|
||||||
HSPLandroidx/appcompat/widget/p;-><init>(Landroid/content/Context;)V
|
|
||||||
HSPLandroidx/appcompat/widget/p;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
|
||||||
HSPLandroidx/appcompat/widget/p;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/p;->r()V
|
|
||||||
HSPLandroidx/appcompat/widget/p;->drawableStateChanged()V
|
|
||||||
HSPLandroidx/appcompat/widget/p;->getEmojiTextViewHelper()Landroidx/appcompat/widget/h;
|
|
||||||
HSPLandroidx/appcompat/widget/p;->getText()Ljava/lang/CharSequence;
|
|
||||||
HSPLandroidx/appcompat/widget/p;->onLayout(ZIIII)V
|
|
||||||
HSPLandroidx/appcompat/widget/p;->onMeasure(II)V
|
|
||||||
HSPLandroidx/appcompat/widget/p;->onTextChanged(Ljava/lang/CharSequence;III)V
|
|
||||||
HSPLandroidx/appcompat/widget/p;->setCompoundDrawables(Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;)V
|
|
||||||
HSPLandroidx/appcompat/widget/p;->setCompoundDrawablesWithIntrinsicBounds(Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;)V
|
|
||||||
HSPLandroidx/appcompat/widget/p;->setFilters([Landroid/text/InputFilter;)V
|
|
||||||
HSPLandroidx/appcompat/widget/p;->setTextAppearance(Landroid/content/Context;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/p;->setTypeface(Landroid/graphics/Typeface;I)V
|
|
||||||
Landroidx/appcompat/widget/q$b;
|
|
||||||
Landroidx/appcompat/widget/q$d;
|
|
||||||
HSPLandroidx/appcompat/widget/q$b;-><init>()V
|
|
||||||
Landroidx/appcompat/widget/q$c;
|
|
||||||
HSPLandroidx/appcompat/widget/q$c;-><init>()V
|
|
||||||
HSPLandroidx/appcompat/widget/q$d;-><init>()V
|
|
||||||
Landroidx/appcompat/widget/q;
|
|
||||||
HSPLandroidx/appcompat/widget/q;-><clinit>()V
|
|
||||||
HSPLandroidx/appcompat/widget/q;-><init>(Landroid/widget/TextView;)V
|
|
||||||
HSPLandroidx/appcompat/widget/q;->j()I
|
|
||||||
HSPLandroidx/appcompat/widget/q;->o(Landroid/util/AttributeSet;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/q;->y()Z
|
|
||||||
Landroidx/appcompat/widget/ContentFrameLayout$a;
|
|
||||||
Landroidx/appcompat/widget/ContentFrameLayout;
|
|
||||||
HSPLandroidx/appcompat/widget/ContentFrameLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
|
||||||
HSPLandroidx/appcompat/widget/ContentFrameLayout;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/ContentFrameLayout;->getMinWidthMajor()Landroid/util/TypedValue;
|
|
||||||
HSPLandroidx/appcompat/widget/ContentFrameLayout;->getMinWidthMinor()Landroid/util/TypedValue;
|
|
||||||
HSPLandroidx/appcompat/widget/ContentFrameLayout;->onAttachedToWindow()V
|
|
||||||
PLandroidx/appcompat/widget/ContentFrameLayout;->onDetachedFromWindow()V
|
|
||||||
HSPLandroidx/appcompat/widget/ContentFrameLayout;->onMeasure(II)V
|
|
||||||
HSPLandroidx/appcompat/widget/ContentFrameLayout;->setAttachListener(Landroidx/appcompat/widget/ContentFrameLayout$a;)V
|
|
||||||
Landroidx/appcompat/widget/u;
|
|
||||||
Landroidx/appcompat/widget/v;
|
|
||||||
HSPLandroidx/appcompat/widget/y;-><init>(Landroid/view/View;)V
|
|
||||||
HSPLandroidx/appcompat/widget/z;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
|
||||||
HSPLandroidx/appcompat/widget/z;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/z;->getVirtualChildCount()I
|
|
||||||
HSPLandroidx/appcompat/widget/z;->p(IIII)V
|
|
||||||
HSPLandroidx/appcompat/widget/z;->s(II)V
|
|
||||||
HSPLandroidx/appcompat/widget/z;->onInitializeAccessibilityNodeInfo(Landroid/view/accessibility/AccessibilityNodeInfo;)V
|
|
||||||
HSPLandroidx/appcompat/widget/z;->onLayout(ZIIII)V
|
|
||||||
HSPLandroidx/appcompat/widget/z;->onMeasure(II)V
|
|
||||||
HSPLandroidx/appcompat/widget/z;->setBaselineAligned(Z)V
|
|
||||||
HSPLandroidx/appcompat/widget/z;->setDividerDrawable(Landroid/graphics/drawable/Drawable;)V
|
|
||||||
Landroidx/appcompat/widget/D$c;
|
|
||||||
Lp/h;
|
|
||||||
Landroidx/appcompat/widget/D;
|
|
||||||
Landroidx/appcompat/widget/E;
|
|
||||||
Landroidx/appcompat/widget/F;
|
|
||||||
HSPLandroidx/appcompat/widget/F;-><init>()V
|
|
||||||
HSPLandroidx/appcompat/widget/F;->a()I
|
|
||||||
HSPLandroidx/appcompat/widget/F;->d()I
|
|
||||||
HSPLandroidx/appcompat/widget/F;->e(II)V
|
|
||||||
HSPLandroidx/appcompat/widget/F;->f(Z)V
|
|
||||||
HSPLandroidx/appcompat/widget/F;->g(II)V
|
|
||||||
Landroidx/appcompat/widget/H;
|
|
||||||
HSPLandroidx/appcompat/widget/H;-><clinit>()V
|
|
||||||
HSPLandroidx/appcompat/widget/H;->a(Landroid/view/View;Landroid/content/Context;)V
|
|
||||||
Landroidx/appcompat/widget/I;
|
|
||||||
HSPLandroidx/appcompat/widget/I;-><clinit>()V
|
|
||||||
HSPLandroidx/appcompat/widget/I;->a(Landroid/content/Context;)Z
|
|
||||||
HSPLandroidx/appcompat/widget/I;->b(Landroid/content/Context;)Landroid/content/Context;
|
|
||||||
Landroidx/appcompat/widget/K;
|
|
||||||
Landroidx/appcompat/widget/L;
|
|
||||||
HSPLandroidx/appcompat/widget/L;-><init>(Landroid/content/Context;Landroid/content/res/TypedArray;)V
|
|
||||||
HSPLandroidx/appcompat/widget/L;->a(IZ)Z
|
|
||||||
HSPLandroidx/appcompat/widget/L;->b(II)I
|
|
||||||
HSPLandroidx/appcompat/widget/L;->c(I)Landroid/content/res/ColorStateList;
|
|
||||||
HSPLandroidx/appcompat/widget/L;->d(II)I
|
|
||||||
HSPLandroidx/appcompat/widget/L;->e(II)I
|
|
||||||
HSPLandroidx/appcompat/widget/L;->f(I)Landroid/graphics/drawable/Drawable;
|
|
||||||
HSPLandroidx/appcompat/widget/L;->g(IF)F
|
|
||||||
HSPLandroidx/appcompat/widget/L;->h(IILandroidx/core/content/res/h$e;)Landroid/graphics/Typeface;
|
|
||||||
HSPLandroidx/appcompat/widget/L;->i(II)I
|
|
||||||
HSPLandroidx/appcompat/widget/L;->j(II)I
|
|
||||||
HSPLandroidx/appcompat/widget/L;->k(II)I
|
|
||||||
HSPLandroidx/appcompat/widget/L;->l(II)I
|
|
||||||
HSPLandroidx/appcompat/widget/L;->m(I)Ljava/lang/String;
|
|
||||||
HSPLandroidx/appcompat/widget/L;->n(I)Ljava/lang/CharSequence;
|
|
||||||
HSPLandroidx/appcompat/widget/L;->o()Landroid/content/res/TypedArray;
|
|
||||||
HSPLandroidx/appcompat/widget/L;->p(I)Z
|
|
||||||
HSPLandroidx/appcompat/widget/L;->q(Landroid/content/Context;I[I)Landroidx/appcompat/widget/L;
|
|
||||||
HSPLandroidx/appcompat/widget/L;->r(Landroid/content/Context;Landroid/util/AttributeSet;[I)Landroidx/appcompat/widget/L;
|
|
||||||
HSPLandroidx/appcompat/widget/L;->s(Landroid/content/Context;Landroid/util/AttributeSet;[III)Landroidx/appcompat/widget/L;
|
|
||||||
HSPLandroidx/appcompat/widget/L;->u()V
|
|
||||||
Landroidx/appcompat/widget/Toolbar$a;
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar$a;-><init>(Landroidx/appcompat/widget/Toolbar;)V
|
|
||||||
Landroidx/appcompat/widget/Toolbar$b;
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar$b;-><init>(Landroidx/appcompat/widget/Toolbar;)V
|
|
||||||
Landroidx/appcompat/widget/Toolbar$f;
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar$f;-><init>(Landroidx/appcompat/widget/Toolbar;)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar$f;->h()Z
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar$f;->k(Landroid/content/Context;Landroidx/appcompat/view/menu/d;)V
|
|
||||||
PLandroidx/appcompat/widget/Toolbar$f;->a(Landroidx/appcompat/view/menu/d;Z)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar$f;->j(Z)V
|
|
||||||
Landroidx/appcompat/widget/Toolbar$g;
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar$g;-><init>(II)V
|
|
||||||
Landroidx/appcompat/widget/Toolbar;
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;-><init>(Landroid/content/Context;Landroid/util/AttributeSet;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->b(Ljava/util/List;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->c(Landroid/view/View;Z)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->checkLayoutParams(Landroid/view/ViewGroup$LayoutParams;)Z
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->f()V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->i()V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->j()V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->k()Landroidx/appcompat/widget/Toolbar$g;
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->o(Landroid/view/View;I)I
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->p(I)I
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->getContentInsetEnd()I
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->getContentInsetStart()I
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetEnd()I
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetLeft()I
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetRight()I
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->getCurrentContentInsetStart()I
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->q(Landroid/view/View;)I
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->getNavigationContentDescription()Ljava/lang/CharSequence;
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->getNavigationIcon()Landroid/graphics/drawable/Drawable;
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->getSubtitle()Ljava/lang/CharSequence;
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->getTitle()Ljava/lang/CharSequence;
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->r(Landroid/view/View;)I
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->s(Ljava/util/List;[I)I
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->getWrapper()Landroidx/appcompat/widget/u;
|
|
||||||
PLandroidx/appcompat/widget/Toolbar;->t()Z
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->w(Landroid/view/View;)Z
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->z(Landroid/view/View;I[II)I
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->A(Landroid/view/View;IIII[I)I
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->B(Landroid/view/View;IIIII)V
|
|
||||||
PLandroidx/appcompat/widget/Toolbar;->onDetachedFromWindow()V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->onLayout(ZIIII)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->onMeasure(II)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->onRtlPropertiesChanged(I)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->setCollapsible(Z)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->F(II)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->setNavigationContentDescription(Ljava/lang/CharSequence;)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->setNavigationIcon(Landroid/graphics/drawable/Drawable;)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->setNavigationOnClickListener(Landroid/view/View$OnClickListener;)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->setPopupTheme(I)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->setSubtitle(Ljava/lang/CharSequence;)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->G(Landroid/content/Context;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->setTitle(Ljava/lang/CharSequence;)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->H(Landroid/content/Context;I)V
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->I()Z
|
|
||||||
HSPLandroidx/appcompat/widget/Toolbar;->J(Landroid/view/View;)Z
|
|
||||||
Landroidx/appcompat/widget/P$a;
|
|
||||||
HSPLandroidx/appcompat/widget/P$a;-><init>(Landroidx/appcompat/widget/P;)V
|
|
||||||
Landroidx/appcompat/widget/P;
|
|
||||||
HSPLandroidx/appcompat/widget/P;-><init>(Landroidx/appcompat/widget/Toolbar;Z)V
|
|
||||||
HSPLandroidx/appcompat/widget/P;-><init>(Landroidx/appcompat/widget/Toolbar;ZII)V
|
|
||||||
HSPLandroidx/appcompat/widget/P;->e()Landroid/content/Context;
|
|
||||||
HSPLandroidx/appcompat/widget/P;->g(I)V
|
|
||||||
HSPLandroidx/appcompat/widget/P;->h(I)V
|
|
||||||
HSPLandroidx/appcompat/widget/P;->l(Landroid/graphics/drawable/Drawable;)V
|
|
||||||
HSPLandroidx/appcompat/widget/P;->o(Ljava/lang/CharSequence;)V
|
|
||||||
HSPLandroidx/appcompat/widget/P;->c(Landroid/view/Window$Callback;)V
|
|
||||||
HSPLandroidx/appcompat/widget/P;->b(Ljava/lang/CharSequence;)V
|
|
||||||
HSPLandroidx/appcompat/widget/P;->q()V
|
|
||||||
Landroidx/appcompat/widget/Q;
|
|
||||||
HSPLandroidx/appcompat/widget/Q;->a(Landroid/view/View;Ljava/lang/CharSequence;)V
|
|
||||||
Landroidx/appcompat/widget/W;
|
|
||||||
HSPLandroidx/appcompat/widget/W;-><clinit>()V
|
|
||||||
HSPLandroidx/appcompat/widget/W;->b()Z
|
|
||||||
HSPLandroidx/appcompat/widget/W;->c()Z
|
|
||||||
Landroidx/appcompat/widget/X;
|
|
||||||
HSPLandroidx/appcompat/widget/X;-><clinit>()V
|
|
||||||
HSPLandroidx/appcompat/widget/X;->a(Landroid/view/View;)Z
|
|
||||||
LE/b;
|
|
||||||
LE/c;
|
|
||||||
HSPLE/c;-><clinit>()V
|
|
||||||
Landroidx/fragment/app/a;
|
|
||||||
Landroidx/fragment/app/y;
|
|
||||||
Landroidx/fragment/app/q$h;
|
|
||||||
HSPLandroidx/fragment/app/a;-><init>(Landroidx/fragment/app/q;)V
|
|
||||||
HSPLandroidx/fragment/app/a;->i(I)V
|
|
||||||
HSPLandroidx/fragment/app/a;->g(ILandroidx/fragment/app/Fragment;Ljava/lang/String;I)V
|
|
||||||
HSPLandroidx/fragment/app/a;->k()V
|
|
||||||
HSPLandroidx/fragment/app/a;->m(Ljava/util/ArrayList;Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/Fragment;
|
|
||||||
HSPLandroidx/fragment/app/a;->a(Ljava/util/ArrayList;Ljava/util/ArrayList;)Z
|
|
||||||
HSPLandroidx/fragment/app/a;->n()V
|
|
||||||
Landroidx/fragment/app/d;
|
|
||||||
Landroidx/fragment/app/E;
|
|
||||||
HSPLandroidx/fragment/app/d;-><init>(Landroid/view/ViewGroup;)V
|
|
||||||
Landroidx/fragment/app/Fragment$a;
|
|
||||||
HSPLandroidx/fragment/app/Fragment$a;-><init>(Landroidx/fragment/app/Fragment;)V
|
|
||||||
Landroidx/fragment/app/Fragment$c;
|
|
||||||
Landroidx/fragment/app/g;
|
|
||||||
HSPLandroidx/fragment/app/Fragment$c;-><init>(Landroidx/fragment/app/Fragment;)V
|
|
||||||
Landroidx/fragment/app/Fragment$d;
|
|
||||||
HSPLandroidx/fragment/app/Fragment$d;-><init>()V
|
|
||||||
Landroidx/fragment/app/Fragment$e;
|
|
||||||
PLandroidx/fragment/app/Fragment$e;->a(Landroid/view/View;)V
|
|
||||||
Landroidx/fragment/app/Fragment;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;-><clinit>()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;-><init>()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->h()Landroidx/fragment/app/g;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->i()Landroidx/fragment/app/Fragment$d;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->equals(Ljava/lang/Object;)Z
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->j()Landroidx/fragment/app/e;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->n()Landroidx/fragment/app/q;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->o()Landroid/content/Context;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->v()Landroid/view/View;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->w(Landroid/os/Bundle;)Landroid/view/LayoutInflater;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->g()Landroidx/lifecycle/g;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->x()I
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->z()Landroidx/fragment/app/Fragment;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->A()Landroidx/fragment/app/q;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->E()F
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->b()Landroidx/savedstate/a;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->M()Landroid/view/View;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->c()Landroidx/lifecycle/D;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->N()V
|
|
||||||
PLandroidx/fragment/app/Fragment;->O()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->P()Z
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->S()Z
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->U(Landroid/os/Bundle;)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->V(Landroid/os/Bundle;)V
|
|
||||||
PLandroidx/fragment/app/Fragment;->b0()V
|
|
||||||
PLandroidx/fragment/app/Fragment;->c0()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->d0(Landroid/os/Bundle;)Landroid/view/LayoutInflater;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->f0(Landroid/content/Context;Landroid/util/AttributeSet;Landroid/os/Bundle;)V
|
|
||||||
PLandroidx/fragment/app/Fragment;->h0()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->j0(Z)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->k0()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->m0()V
|
|
||||||
PLandroidx/fragment/app/Fragment;->n0()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->o0(Landroid/view/View;Landroid/os/Bundle;)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->p0(Landroid/os/Bundle;)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->q0(Landroid/os/Bundle;)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->r0()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->t0(Landroid/os/Bundle;)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->u0(Landroid/view/Menu;Landroid/view/MenuInflater;)Z
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->v0(Landroid/view/LayoutInflater;Landroid/view/ViewGroup;Landroid/os/Bundle;)V
|
|
||||||
PLandroidx/fragment/app/Fragment;->w0()V
|
|
||||||
PLandroidx/fragment/app/Fragment;->x0()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->y0(Landroid/os/Bundle;)Landroid/view/LayoutInflater;
|
|
||||||
PLandroidx/fragment/app/Fragment;->B0()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->C0(Landroid/view/Menu;)Z
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->D0()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->E0()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->G0()V
|
|
||||||
PLandroidx/fragment/app/Fragment;->H0()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->I0()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->L0()Landroid/content/Context;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->M0()Landroid/view/View;
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->N0(Landroid/os/Bundle;)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->O0()V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->P0(Landroid/os/Bundle;)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->Q0(IIII)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->R0(Landroid/view/View;)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->S0(I)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->T0(Z)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->U0(F)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->V0(Ljava/util/ArrayList;Ljava/util/ArrayList;)V
|
|
||||||
HSPLandroidx/fragment/app/Fragment;->toString()Ljava/lang/String;
|
|
||||||
Landroidx/fragment/app/e;
|
|
||||||
HSPLandroidx/fragment/app/g;-><init>()V
|
|
||||||
Landroidx/fragment/app/h;
|
|
||||||
PLandroidx/fragment/app/h;->a(Landroid/view/View;)V
|
|
||||||
HSPLandroidx/fragment/app/h;->addView(Landroid/view/View;ILandroid/view/ViewGroup$LayoutParams;)V
|
|
||||||
HSPLandroidx/fragment/app/h;->dispatchDraw(Landroid/graphics/Canvas;)V
|
|
||||||
HSPLandroidx/fragment/app/h;->drawChild(Landroid/graphics/Canvas;Landroid/view/View;J)Z
|
|
||||||
PLandroidx/fragment/app/h;->removeView(Landroid/view/View;)V
|
|
||||||
Landroidx/fragment/app/i;
|
|
||||||
HSPLandroidx/fragment/app/i;-><clinit>()V
|
|
||||||
HSPLandroidx/fragment/app/i;-><init>()V
|
|
||||||
HSPLandroidx/fragment/app/i;->b(Ljava/lang/ClassLoader;Ljava/lang/String;)Z
|
|
||||||
HSPLandroidx/fragment/app/i;->c(Ljava/lang/ClassLoader;Ljava/lang/String;)Ljava/lang/Class;
|
|
||||||
Landroidx/fragment/app/j;
|
|
||||||
Landroidx/fragment/app/k;
|
|
||||||
HSPLandroidx/fragment/app/k;-><init>(Landroidx/fragment/app/q;)V
|
|
||||||
HSPLandroidx/fragment/app/k;->onCreateView(Landroid/view/View;Ljava/lang/String;Landroid/content/Context;Landroid/util/AttributeSet;)Landroid/view/View;
|
|
||||||
Landroidx/fragment/app/l;
|
|
||||||
HSPLandroidx/fragment/app/l;-><init>(Landroidx/fragment/app/q;)V
|
|
||||||
HSPLandroidx/fragment/app/l;->a(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Z)V
|
|
||||||
HSPLandroidx/fragment/app/l;->b(Landroidx/fragment/app/Fragment;Z)V
|
|
||||||
HSPLandroidx/fragment/app/l;->c(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Z)V
|
|
||||||
PLandroidx/fragment/app/l;->d(Landroidx/fragment/app/Fragment;Z)V
|
|
||||||
PLandroidx/fragment/app/l;->e(Landroidx/fragment/app/Fragment;Z)V
|
|
||||||
HSPLandroidx/fragment/app/l;->f(Landroidx/fragment/app/Fragment;Z)V
|
|
||||||
HSPLandroidx/fragment/app/l;->g(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Z)V
|
|
||||||
HSPLandroidx/fragment/app/l;->h(Landroidx/fragment/app/Fragment;Z)V
|
|
||||||
HSPLandroidx/fragment/app/l;->j(Landroidx/fragment/app/Fragment;Z)V
|
|
||||||
PLandroidx/fragment/app/l;->k(Landroidx/fragment/app/Fragment;Z)V
|
|
||||||
HSPLandroidx/fragment/app/l;->l(Landroidx/fragment/app/Fragment;Landroid/view/View;Landroid/os/Bundle;Z)V
|
|
||||||
PLandroidx/fragment/app/l;->m(Landroidx/fragment/app/Fragment;Z)V
|
|
||||||
Landroidx/fragment/app/q$a;
|
|
||||||
HSPLandroidx/fragment/app/q$a;-><init>(Landroidx/fragment/app/q;Z)V
|
|
||||||
Landroidx/fragment/app/q$b;
|
|
||||||
Landroidx/core/view/l;
|
|
||||||
HSPLandroidx/fragment/app/q$b;-><init>(Landroidx/fragment/app/q;)V
|
|
||||||
Landroidx/fragment/app/q$c;
|
|
||||||
HSPLandroidx/fragment/app/q$c;-><init>(Landroidx/fragment/app/q;)V
|
|
||||||
Landroidx/fragment/app/q$d;
|
|
||||||
Landroidx/fragment/app/F;
|
|
||||||
HSPLandroidx/fragment/app/q$d;-><init>(Landroidx/fragment/app/q;)V
|
|
||||||
Landroidx/fragment/app/FragmentManager$6;
|
|
||||||
Landroidx/fragment/app/q$f;
|
|
||||||
Landroidx/fragment/app/u;
|
|
||||||
Landroidx/fragment/app/q;
|
|
||||||
HSPLandroidx/fragment/app/q;-><clinit>()V
|
|
||||||
HSPLandroidx/fragment/app/q;-><init>()V
|
|
||||||
HSPLandroidx/fragment/app/q;->h(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/w;
|
|
||||||
HSPLandroidx/fragment/app/q;->i(Landroidx/fragment/app/u;)V
|
|
||||||
HSPLandroidx/fragment/app/q;->j(Landroidx/fragment/app/j;Landroidx/fragment/app/g;Landroidx/fragment/app/Fragment;)V
|
|
||||||
HSPLandroidx/fragment/app/q;->l()Landroidx/fragment/app/y;
|
|
||||||
HSPLandroidx/fragment/app/q;->m()Z
|
|
||||||
HSPLandroidx/fragment/app/q;->n()V
|
|
||||||
PLandroidx/fragment/app/q;->o()V
|
|
||||||
HSPLandroidx/fragment/app/q;->p()Ljava/util/Set;
|
|
||||||
HSPLandroidx/fragment/app/q;->q(Ljava/util/ArrayList;II)Ljava/util/Set;
|
|
||||||
HSPLandroidx/fragment/app/q;->r(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/w;
|
|
||||||
HSPLandroidx/fragment/app/q;->t()V
|
|
||||||
HSPLandroidx/fragment/app/q;->v()V
|
|
||||||
HSPLandroidx/fragment/app/q;->w(Landroid/view/Menu;Landroid/view/MenuInflater;)Z
|
|
||||||
PLandroidx/fragment/app/q;->x()V
|
|
||||||
PLandroidx/fragment/app/q;->y()V
|
|
||||||
HSPLandroidx/fragment/app/q;->C(Landroidx/fragment/app/Fragment;)V
|
|
||||||
PLandroidx/fragment/app/q;->D()V
|
|
||||||
HSPLandroidx/fragment/app/q;->E(Landroid/view/Menu;)Z
|
|
||||||
HSPLandroidx/fragment/app/q;->F()V
|
|
||||||
HSPLandroidx/fragment/app/q;->G()V
|
|
||||||
HSPLandroidx/fragment/app/q;->H()V
|
|
||||||
HSPLandroidx/fragment/app/q;->I(I)V
|
|
||||||
PLandroidx/fragment/app/q;->J()V
|
|
||||||
HSPLandroidx/fragment/app/q;->K()V
|
|
||||||
HSPLandroidx/fragment/app/q;->L()V
|
|
||||||
PLandroidx/fragment/app/q;->M()V
|
|
||||||
HSPLandroidx/fragment/app/q;->N(Z)V
|
|
||||||
HSPLandroidx/fragment/app/q;->O(Z)Z
|
|
||||||
HSPLandroidx/fragment/app/q;->Q(Ljava/util/ArrayList;Ljava/util/ArrayList;II)V
|
|
||||||
HSPLandroidx/fragment/app/q;->R(Ljava/util/ArrayList;Ljava/util/ArrayList;II)V
|
|
||||||
HSPLandroidx/fragment/app/q;->S(Ljava/lang/String;)Landroidx/fragment/app/Fragment;
|
|
||||||
HSPLandroidx/fragment/app/q;->T(I)Landroidx/fragment/app/Fragment;
|
|
||||||
HSPLandroidx/fragment/app/q;->Y(Ljava/util/ArrayList;Ljava/util/ArrayList;)Z
|
|
||||||
HSPLandroidx/fragment/app/q;->Z()I
|
|
||||||
HSPLandroidx/fragment/app/q;->a0(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/t;
|
|
||||||
HSPLandroidx/fragment/app/q;->b0()Landroidx/fragment/app/g;
|
|
||||||
HSPLandroidx/fragment/app/q;->c0(Landroidx/fragment/app/Fragment;)Landroid/view/ViewGroup;
|
|
||||||
HSPLandroidx/fragment/app/q;->d0()Landroidx/fragment/app/i;
|
|
||||||
HSPLandroidx/fragment/app/q;->e0()Landroidx/fragment/app/j;
|
|
||||||
HSPLandroidx/fragment/app/q;->f0()Landroidx/fragment/app/l;
|
|
||||||
HSPLandroidx/fragment/app/q;->g0()Landroidx/fragment/app/Fragment;
|
|
||||||
HSPLandroidx/fragment/app/q;->h0()Landroidx/fragment/app/Fragment;
|
|
||||||
HSPLandroidx/fragment/app/q;->i0()Landroidx/fragment/app/F;
|
|
||||||
HSPLandroidx/fragment/app/q;->j0()LF/c$c;
|
|
||||||
HSPLandroidx/fragment/app/q;->k0(Landroid/view/View;)Landroidx/fragment/app/Fragment;
|
|
||||||
HSPLandroidx/fragment/app/q;->l0(Landroidx/fragment/app/Fragment;)Landroidx/lifecycle/D;
|
|
||||||
PLandroidx/fragment/app/q;->o0()Z
|
|
||||||
HSPLandroidx/fragment/app/q;->p0(I)Z
|
|
||||||
HSPLandroidx/fragment/app/q;->q0(Landroidx/fragment/app/Fragment;)Z
|
|
||||||
HSPLandroidx/fragment/app/q;->t0(Landroidx/fragment/app/Fragment;)Z
|
|
||||||
HSPLandroidx/fragment/app/q;->u0(Landroidx/fragment/app/Fragment;)Z
|
|
||||||
HSPLandroidx/fragment/app/q;->v0(I)Z
|
|
||||||
HSPLandroidx/fragment/app/q;->w0()Z
|
|
||||||
HSPLandroidx/fragment/app/q;->x0(IZ)V
|
|
||||||
HSPLandroidx/fragment/app/q;->y0()V
|
|
||||||
HSPLandroidx/fragment/app/q;->A0(Landroidx/fragment/app/w;)V
|
|
||||||
HSPLandroidx/fragment/app/q;->C0(Ljava/util/ArrayList;Ljava/util/ArrayList;)V
|
|
||||||
HSPLandroidx/fragment/app/q;->H0(Landroidx/fragment/app/Fragment;Z)V
|
|
||||||
HSPLandroidx/fragment/app/q;->J0(Landroidx/fragment/app/Fragment;)V
|
|
||||||
HSPLandroidx/fragment/app/q;->M0()V
|
|
||||||
HSPLandroidx/fragment/app/q;->N0()V
|
|
||||||
Landroidx/fragment/app/r;
|
|
||||||
HSPLandroidx/fragment/app/r;-><init>()V
|
|
||||||
Landroidx/fragment/app/t$a;
|
|
||||||
Landroidx/lifecycle/A$b;
|
|
||||||
HSPLandroidx/fragment/app/t$a;-><init>()V
|
|
||||||
HSPLandroidx/fragment/app/t$a;->b(Ljava/lang/Class;)Landroidx/lifecycle/z;
|
|
||||||
Landroidx/fragment/app/t;
|
|
||||||
Landroidx/lifecycle/z;
|
|
||||||
HSPLandroidx/fragment/app/t;-><clinit>()V
|
|
||||||
HSPLandroidx/fragment/app/t;-><init>(Z)V
|
|
||||||
PLandroidx/fragment/app/t;->f(Landroidx/fragment/app/Fragment;)V
|
|
||||||
PLandroidx/fragment/app/t;->g(Ljava/lang/String;)V
|
|
||||||
HSPLandroidx/fragment/app/t;->i(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/t;
|
|
||||||
HSPLandroidx/fragment/app/t;->k(Landroidx/fragment/app/Fragment;)Landroidx/lifecycle/D;
|
|
||||||
PLandroidx/fragment/app/t;->d()V
|
|
||||||
HSPLandroidx/fragment/app/t;->m(Z)V
|
|
||||||
PLandroidx/fragment/app/t;->n(Landroidx/fragment/app/Fragment;)Z
|
|
||||||
Landroidx/fragment/app/w$a;
|
|
||||||
HSPLandroidx/fragment/app/w$a;-><init>(Landroidx/fragment/app/w;Landroid/view/View;)V
|
|
||||||
HSPLandroidx/fragment/app/w$a;->onViewAttachedToWindow(Landroid/view/View;)V
|
|
||||||
Landroidx/fragment/app/w$b;
|
|
||||||
HSPLandroidx/fragment/app/w$b;-><clinit>()V
|
|
||||||
Landroidx/fragment/app/w;
|
|
||||||
HSPLandroidx/fragment/app/w;-><init>(Landroidx/fragment/app/l;Landroidx/fragment/app/x;Landroidx/fragment/app/Fragment;)V
|
|
||||||
HSPLandroidx/fragment/app/w;->a()V
|
|
||||||
HSPLandroidx/fragment/app/w;->b()V
|
|
||||||
HSPLandroidx/fragment/app/w;->c()V
|
|
||||||
HSPLandroidx/fragment/app/w;->d()I
|
|
||||||
HSPLandroidx/fragment/app/w;->e()V
|
|
||||||
HSPLandroidx/fragment/app/w;->f()V
|
|
||||||
PLandroidx/fragment/app/w;->g()V
|
|
||||||
PLandroidx/fragment/app/w;->h()V
|
|
||||||
PLandroidx/fragment/app/w;->i()V
|
|
||||||
HSPLandroidx/fragment/app/w;->j()V
|
|
||||||
HSPLandroidx/fragment/app/w;->k()Landroidx/fragment/app/Fragment;
|
|
||||||
HSPLandroidx/fragment/app/w;->m()V
|
|
||||||
PLandroidx/fragment/app/w;->n()V
|
|
||||||
HSPLandroidx/fragment/app/w;->o()V
|
|
||||||
PLandroidx/fragment/app/w;->r()V
|
|
||||||
HSPLandroidx/fragment/app/w;->s(I)V
|
|
||||||
HSPLandroidx/fragment/app/w;->t()V
|
|
||||||
PLandroidx/fragment/app/w;->u()V
|
|
||||||
Landroidx/fragment/app/x;
|
|
||||||
HSPLandroidx/fragment/app/x;-><init>()V
|
|
||||||
HSPLandroidx/fragment/app/x;->a(Landroidx/fragment/app/Fragment;)V
|
|
||||||
HSPLandroidx/fragment/app/x;->b()V
|
|
||||||
HSPLandroidx/fragment/app/x;->c(Ljava/lang/String;)Z
|
|
||||||
HSPLandroidx/fragment/app/x;->d(I)V
|
|
||||||
HSPLandroidx/fragment/app/x;->e(Ljava/lang/String;)Landroidx/fragment/app/Fragment;
|
|
||||||
HSPLandroidx/fragment/app/x;->f(I)Landroidx/fragment/app/Fragment;
|
|
||||||
HSPLandroidx/fragment/app/x;->h(Landroidx/fragment/app/Fragment;)I
|
|
||||||
HSPLandroidx/fragment/app/x;->i()Ljava/util/List;
|
|
||||||
HSPLandroidx/fragment/app/x;->j()Ljava/util/List;
|
|
||||||
HSPLandroidx/fragment/app/x;->l(Ljava/lang/String;)Landroidx/fragment/app/w;
|
|
||||||
HSPLandroidx/fragment/app/x;->m()Ljava/util/List;
|
|
||||||
PLandroidx/fragment/app/x;->n()Landroidx/fragment/app/t;
|
|
||||||
HSPLandroidx/fragment/app/x;->p(Landroidx/fragment/app/w;)V
|
|
||||||
PLandroidx/fragment/app/x;->q(Landroidx/fragment/app/w;)V
|
|
||||||
HSPLandroidx/fragment/app/x;->r()V
|
|
||||||
HSPLandroidx/fragment/app/x;->y(Landroidx/fragment/app/t;)V
|
|
||||||
Landroidx/fragment/app/y$a;
|
|
||||||
HSPLandroidx/fragment/app/y$a;-><init>(ILandroidx/fragment/app/Fragment;)V
|
|
||||||
HSPLandroidx/fragment/app/y$a;-><init>(ILandroidx/fragment/app/Fragment;Z)V
|
|
||||||
HSPLandroidx/fragment/app/y;-><init>(Landroidx/fragment/app/i;Ljava/lang/ClassLoader;)V
|
|
||||||
HSPLandroidx/fragment/app/y;->d(Landroidx/fragment/app/y$a;)V
|
|
||||||
HSPLandroidx/fragment/app/y;->g(ILandroidx/fragment/app/Fragment;Ljava/lang/String;I)V
|
|
||||||
HSPLandroidx/fragment/app/y;->h(Z)Landroidx/fragment/app/y;
|
|
||||||
Landroidx/fragment/app/C;
|
|
||||||
HSPLandroidx/fragment/app/C;-><init>(Landroidx/fragment/app/Fragment;Landroidx/lifecycle/D;)V
|
|
||||||
HSPLandroidx/fragment/app/C;->g()Landroidx/lifecycle/g;
|
|
||||||
HSPLandroidx/fragment/app/C;->b()Landroidx/savedstate/a;
|
|
||||||
HSPLandroidx/fragment/app/C;->d(Landroidx/lifecycle/g$a;)V
|
|
||||||
HSPLandroidx/fragment/app/C;->e()V
|
|
||||||
HSPLandroidx/fragment/app/C;->h(Landroid/os/Bundle;)V
|
|
||||||
PLandroidx/fragment/app/C;->i(Landroid/os/Bundle;)V
|
|
||||||
Landroidx/fragment/app/E$a;
|
|
||||||
HSPLandroidx/fragment/app/E$a;-><init>(Landroidx/fragment/app/E;Landroidx/fragment/app/E$d;)V
|
|
||||||
HSPLandroidx/fragment/app/E$a;->run()V
|
|
||||||
Landroidx/fragment/app/E$b;
|
|
||||||
HSPLandroidx/fragment/app/E$b;-><init>(Landroidx/fragment/app/E;Landroidx/fragment/app/E$d;)V
|
|
||||||
HSPLandroidx/fragment/app/E$b;->run()V
|
|
||||||
Landroidx/fragment/app/E$c;
|
|
||||||
HSPLandroidx/fragment/app/E$c;-><clinit>()V
|
|
||||||
Landroidx/fragment/app/E$d;
|
|
||||||
Landroidx/fragment/app/E$e;
|
|
||||||
HSPLandroidx/fragment/app/E$d;-><init>(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;Landroidx/fragment/app/w;Landroidx/core/os/b;)V
|
|
||||||
HSPLandroidx/fragment/app/E$d;->c()V
|
|
||||||
HSPLandroidx/fragment/app/E$d;->l()V
|
|
||||||
Landroidx/fragment/app/E$e$a;
|
|
||||||
Landroidx/core/os/b$a;
|
|
||||||
HSPLandroidx/fragment/app/E$e$a;-><init>(Landroidx/fragment/app/E$e;)V
|
|
||||||
Landroidx/fragment/app/E$e$b;
|
|
||||||
HSPLandroidx/fragment/app/E$e$b;-><clinit>()V
|
|
||||||
HSPLandroidx/fragment/app/E$e$b;-><init>(Ljava/lang/String;I)V
|
|
||||||
HSPLandroidx/fragment/app/E$e$b;->values()[Landroidx/fragment/app/E$e$b;
|
|
||||||
Landroidx/fragment/app/E$e$c;
|
|
||||||
HSPLandroidx/fragment/app/E$e$c;-><clinit>()V
|
|
||||||
HSPLandroidx/fragment/app/E$e$c;-><init>(Ljava/lang/String;I)V
|
|
||||||
HSPLandroidx/fragment/app/E$e$c;->a(Landroid/view/View;)V
|
|
||||||
HSPLandroidx/fragment/app/E$e$c;->b(I)Landroidx/fragment/app/E$e$c;
|
|
||||||
HSPLandroidx/fragment/app/E$e$c;->values()[Landroidx/fragment/app/E$e$c;
|
|
||||||
HSPLandroidx/fragment/app/E$e;-><init>(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;Landroidx/fragment/app/Fragment;Landroidx/core/os/b;)V
|
|
||||||
HSPLandroidx/fragment/app/E$e;->a(Ljava/lang/Runnable;)V
|
|
||||||
HSPLandroidx/fragment/app/E$e;->b()V
|
|
||||||
HSPLandroidx/fragment/app/E$e;->c()V
|
|
||||||
HSPLandroidx/fragment/app/E$e;->e()Landroidx/fragment/app/E$e$c;
|
|
||||||
HSPLandroidx/fragment/app/E$e;->f()Landroidx/fragment/app/Fragment;
|
|
||||||
HSPLandroidx/fragment/app/E$e;->g()Landroidx/fragment/app/E$e$b;
|
|
||||||
HSPLandroidx/fragment/app/E$e;->h()Z
|
|
||||||
HSPLandroidx/fragment/app/E$e;->k(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;)V
|
|
||||||
HSPLandroidx/fragment/app/E;-><init>(Landroid/view/ViewGroup;)V
|
|
||||||
HSPLandroidx/fragment/app/E;->a(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/E$e$b;Landroidx/fragment/app/w;)V
|
|
||||||
HSPLandroidx/fragment/app/E;->b(Landroidx/fragment/app/E$e$c;Landroidx/fragment/app/w;)V
|
|
||||||
PLandroidx/fragment/app/E;->d(Landroidx/fragment/app/w;)V
|
|
||||||
HSPLandroidx/fragment/app/E;->g()V
|
|
||||||
HSPLandroidx/fragment/app/E;->h(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/E$e;
|
|
||||||
HSPLandroidx/fragment/app/E;->i(Landroidx/fragment/app/Fragment;)Landroidx/fragment/app/E$e;
|
|
||||||
HSPLandroidx/fragment/app/E;->j()V
|
|
||||||
HSPLandroidx/fragment/app/E;->l(Landroidx/fragment/app/w;)Landroidx/fragment/app/E$e$b;
|
|
||||||
HSPLandroidx/fragment/app/E;->n(Landroid/view/ViewGroup;Landroidx/fragment/app/q;)Landroidx/fragment/app/E;
|
|
||||||
HSPLandroidx/fragment/app/E;->o(Landroid/view/ViewGroup;Landroidx/fragment/app/F;)Landroidx/fragment/app/E;
|
|
||||||
HSPLandroidx/fragment/app/E;->p()V
|
|
||||||
HSPLandroidx/fragment/app/E;->q()V
|
|
||||||
HSPLandroidx/fragment/app/E;->r(Z)V
|
|
||||||
LF/c$a;
|
|
||||||
HSPLF/c$a;->a()[LF/c$a;
|
|
||||||
HSPLF/c$a;-><clinit>()V
|
|
||||||
HSPLF/c$a;-><init>(Ljava/lang/String;I)V
|
|
||||||
LF/c$c$a;
|
|
||||||
HSPLF/c$c$a;-><init>()V
|
|
||||||
HSPLF/c$c$a;-><init>(Lt0/d;)V
|
|
||||||
LF/c$c;
|
|
||||||
HSPLF/c$c;-><clinit>()V
|
|
||||||
HSPLF/c$c;-><init>(Ljava/util/Set;LF/c$b;Ljava/util/Map;)V
|
|
||||||
HSPLF/c$c;->a()Ljava/util/Set;
|
|
||||||
LF/c;
|
|
||||||
HSPLF/c;-><clinit>()V
|
|
||||||
HSPLF/c;-><init>()V
|
|
||||||
HSPLF/c;->b(Landroidx/fragment/app/Fragment;)LF/c$c;
|
|
||||||
HSPLF/c;->e(LF/d;)V
|
|
||||||
LF/d;
|
|
||||||
HSPLF/d;-><init>(Landroidx/fragment/app/Fragment;Ljava/lang/String;)V
|
|
||||||
Landroidx/lifecycle/d;
|
|
||||||
HSPLandroidx/lifecycle/d;-><init>()V
|
|
||||||
HSPLandroidx/lifecycle/d;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
|
|
||||||
PLandroidx/lifecycle/d;->onActivityDestroyed(Landroid/app/Activity;)V
|
|
||||||
PLandroidx/lifecycle/d;->onActivityPaused(Landroid/app/Activity;)V
|
|
||||||
HSPLandroidx/lifecycle/d;->onActivityResumed(Landroid/app/Activity;)V
|
|
||||||
HSPLandroidx/lifecycle/d;->onActivityStarted(Landroid/app/Activity;)V
|
|
||||||
PLandroidx/lifecycle/d;->onActivityStopped(Landroid/app/Activity;)V
|
|
||||||
Landroidx/lifecycle/i$a;
|
|
||||||
HSPLandroidx/lifecycle/i$a;-><init>()V
|
|
||||||
HSPLandroidx/lifecycle/i$a;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
|
|
||||||
Landroidx/lifecycle/i;
|
|
||||||
HSPLandroidx/lifecycle/i;-><clinit>()V
|
|
||||||
HSPLandroidx/lifecycle/i;->a(Landroid/content/Context;)V
|
|
||||||
Landroidx/lifecycle/m$b;
|
|
||||||
HSPLandroidx/lifecycle/m$b;-><init>(Landroidx/lifecycle/k;Landroidx/lifecycle/g$b;)V
|
|
||||||
HSPLandroidx/lifecycle/m$b;->a(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
|
|
||||||
Landroidx/lifecycle/m;
|
|
||||||
Landroidx/lifecycle/g;
|
|
||||||
HSPLandroidx/lifecycle/m;-><init>(Landroidx/lifecycle/l;)V
|
|
||||||
HSPLandroidx/lifecycle/m;-><init>(Landroidx/lifecycle/l;Z)V
|
|
||||||
HSPLandroidx/lifecycle/m;->a(Landroidx/lifecycle/k;)V
|
|
||||||
HPLandroidx/lifecycle/m;->d(Landroidx/lifecycle/l;)V
|
|
||||||
HSPLandroidx/lifecycle/m;->e(Landroidx/lifecycle/k;)Landroidx/lifecycle/g$b;
|
|
||||||
HSPLandroidx/lifecycle/m;->f(Ljava/lang/String;)V
|
|
||||||
HSPLandroidx/lifecycle/m;->g(Landroidx/lifecycle/l;)V
|
|
||||||
HSPLandroidx/lifecycle/m;->b()Landroidx/lifecycle/g$b;
|
|
||||||
HSPLandroidx/lifecycle/m;->h(Landroidx/lifecycle/g$a;)V
|
|
||||||
HSPLandroidx/lifecycle/m;->i()Z
|
|
||||||
HSPLandroidx/lifecycle/m;->j(Landroidx/lifecycle/g$b;)V
|
|
||||||
HSPLandroidx/lifecycle/m;->k()V
|
|
||||||
HSPLandroidx/lifecycle/m;->l(Landroidx/lifecycle/g$b;)V
|
|
||||||
HSPLandroidx/lifecycle/m;->c(Landroidx/lifecycle/k;)V
|
|
||||||
HSPLandroidx/lifecycle/m;->m()V
|
|
||||||
Landroidx/lifecycle/LiveData$a;
|
|
||||||
HSPLandroidx/lifecycle/LiveData$a;-><init>(Landroidx/lifecycle/LiveData;)V
|
|
||||||
HSPLandroidx/lifecycle/LiveData$a;->run()V
|
|
||||||
Landroidx/lifecycle/LiveData$LifecycleBoundObserver;
|
|
||||||
Landroidx/lifecycle/LiveData$b;
|
|
||||||
PLandroidx/lifecycle/LiveData$LifecycleBoundObserver;->i()V
|
|
||||||
HSPLandroidx/lifecycle/LiveData$LifecycleBoundObserver;->h(Landroidx/lifecycle/l;Landroidx/lifecycle/g$a;)V
|
|
||||||
HSPLandroidx/lifecycle/LiveData$LifecycleBoundObserver;->j()Z
|
|
||||||
HSPLandroidx/lifecycle/LiveData$b;->d(Z)V
|
|
||||||
HSPLandroidx/lifecycle/LiveData$b;->i()V
|
|
||||||
Landroidx/lifecycle/LiveData;
|
|
||||||
HSPLandroidx/lifecycle/LiveData;-><clinit>()V
|
|
||||||
HSPLandroidx/lifecycle/LiveData;-><init>()V
|
|
||||||
HSPLandroidx/lifecycle/LiveData;->a(Ljava/lang/String;)V
|
|
||||||
HSPLandroidx/lifecycle/LiveData;->b(I)V
|
|
||||||
HSPLandroidx/lifecycle/LiveData;->c(Landroidx/lifecycle/LiveData$b;)V
|
|
||||||
HSPLandroidx/lifecycle/LiveData;->d(Landroidx/lifecycle/LiveData$b;)V
|
|
||||||
HSPLandroidx/lifecycle/LiveData;->e()V
|
|
||||||
HSPLandroidx/lifecycle/LiveData;->f()V
|
|
||||||
HSPLandroidx/lifecycle/LiveData;->g(Landroidx/lifecycle/q;)V
|
|
||||||
HSPLandroidx/lifecycle/LiveData;->h(Ljava/lang/Object;)V
|
|
||||||
Landroidx/lifecycle/p;
|
|
||||||
HSPLandroidx/lifecycle/p;-><init>()V
|
|
||||||
HSPLandroidx/lifecycle/p;->h(Ljava/lang/Object;)V
|
|
||||||
Landroidx/lifecycle/ProcessLifecycleInitializer;
|
|
||||||
LJ/a;
|
|
||||||
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;-><init>()V
|
|
||||||
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->c(Landroid/content/Context;)Landroidx/lifecycle/l;
|
|
||||||
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->b(Landroid/content/Context;)Ljava/lang/Object;
|
|
||||||
HSPLandroidx/lifecycle/ProcessLifecycleInitializer;->a()Ljava/util/List;
|
|
||||||
Landroidx/lifecycle/t;
|
|
||||||
HSPLandroidx/lifecycle/t;-><clinit>()V
|
|
||||||
HSPLandroidx/lifecycle/t;-><init>()V
|
|
||||||
HSPLandroidx/lifecycle/t;->g()Landroidx/lifecycle/g;
|
|
||||||
Landroidx/lifecycle/u$c;
|
|
||||||
HSPLandroidx/lifecycle/u$c;-><init>()V
|
|
||||||
HSPLandroidx/lifecycle/u$c;->onActivityCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
|
|
||||||
PLandroidx/lifecycle/u$c;->onActivityDestroyed(Landroid/app/Activity;)V
|
|
||||||
PLandroidx/lifecycle/u$c;->onActivityPaused(Landroid/app/Activity;)V
|
|
||||||
HSPLandroidx/lifecycle/u$c;->onActivityPostCreated(Landroid/app/Activity;Landroid/os/Bundle;)V
|
|
||||||
HSPLandroidx/lifecycle/u$c;->onActivityPostResumed(Landroid/app/Activity;)V
|
|
||||||
HSPLandroidx/lifecycle/u$c;->onActivityPostStarted(Landroid/app/Activity;)V
|
|
||||||
PLandroidx/lifecycle/u$c;->onActivityPreDestroyed(Landroid/app/Activity;)V
|
|
||||||
PLandroidx/lifecycle/u$c;->onActivityPrePaused(Landroid/app/Activity;)V
|
|
||||||
PLandroidx/lifecycle/u$c;->onActivityPreStopped(Landroid/app/Activity;)V
|
|
||||||
HSPLandroidx/lifecycle/u$c;->onActivityResumed(Landroid/app/Activity;)V
|
|
||||||
HSPLandroidx/lifecycle/u$c;->onActivityStarted(Landroid/app/Activity;)V
|
|
||||||
PLandroidx/lifecycle/u$c;->onActivityStopped(Landroid/app/Activity;)V
|
|
||||||
HSPLandroidx/lifecycle/u$c;->registerIn(Landroid/app/Activity;)V
|
|
||||||
Landroidx/lifecycle/u;
|
|
||||||
HSPLandroidx/lifecycle/u;-><init>()V
|
|
||||||
HSPLandroidx/lifecycle/u;->a(Landroidx/lifecycle/g$a;)V
|
|
||||||
HSPLandroidx/lifecycle/u;->b(Landroidx/lifecycle/u$a;)V
|
|
||||||
HSPLandroidx/lifecycle/u;->c(Landroidx/lifecycle/u$a;)V
|
|
||||||
HSPLandroidx/lifecycle/u;->d(Landroidx/lifecycle/u$a;)V
|
|
||||||
HSPLandroidx/lifecycle/u;->onActivityCreated(Landroid/os/Bundle;)V
|
|
||||||
PLandroidx/lifecycle/u;->onDestroy()V
|
|
||||||
PLandroidx/lifecycle/u;->onPause()V
|
|
||||||
HSPLandroidx/lifecycle/u;->onResume()V
|
|
||||||
HSPLandroidx/lifecycle/u;->onStart()V
|
|
||||||
PLandroidx/lifecycle/u;->onStop()V
|
|
||||||
HSPLandroidx/lifecycle/z;-><init>()V
|
|
||||||
PLandroidx/lifecycle/z;->a()V
|
|
||||||
PLandroidx/lifecycle/z;->d()V
|
|
||||||
Landroidx/lifecycle/A;
|
|
||||||
HSPLandroidx/lifecycle/A;-><init>(Landroidx/lifecycle/D;Landroidx/lifecycle/A$b;)V
|
|
||||||
HSPLandroidx/lifecycle/A;->a(Ljava/lang/Class;)Landroidx/lifecycle/z;
|
|
||||||
HSPLandroidx/lifecycle/A;->b(Ljava/lang/String;Ljava/lang/Class;)Landroidx/lifecycle/z;
|
|
||||||
Landroidx/lifecycle/D;
|
|
||||||
HSPLandroidx/lifecycle/D;-><init>()V
|
|
||||||
PLandroidx/lifecycle/D;->a()V
|
|
||||||
HSPLandroidx/lifecycle/D;->b(Ljava/lang/String;)Landroidx/lifecycle/z;
|
|
||||||
HSPLandroidx/lifecycle/D;->d(Ljava/lang/String;Landroidx/lifecycle/z;)V
|
|
||||||
Landroidx/lifecycle/F;
|
|
||||||
HSPLandroidx/lifecycle/F;->a(Landroid/view/View;Landroidx/lifecycle/l;)V
|
|
||||||
Landroidx/lifecycle/G;
|
|
||||||
HSPLandroidx/lifecycle/G;->a(Landroid/view/View;Landroidx/lifecycle/E;)V
|
|
||||||
Landroidx/startup/a;
|
|
||||||
HSPLandroidx/startup/a;-><clinit>()V
|
|
||||||
HSPLandroidx/startup/a;-><init>(Landroid/content/Context;)V
|
|
||||||
HSPLandroidx/startup/a;->a()V
|
|
||||||
HSPLandroidx/startup/a;->b(Landroid/os/Bundle;)V
|
|
||||||
HSPLandroidx/startup/a;->c(Ljava/lang/Class;)Ljava/lang/Object;
|
|
||||||
HSPLandroidx/startup/a;->d(Ljava/lang/Class;Ljava/util/Set;)Ljava/lang/Object;
|
|
||||||
HSPLandroidx/startup/a;->e(Landroid/content/Context;)Landroidx/startup/a;
|
|
||||||
HSPLandroidx/startup/a;->f(Ljava/lang/Class;)Ljava/lang/Object;
|
|
||||||
HSPLandroidx/startup/a;->g(Ljava/lang/Class;)Z
|
|
||||||
Landroidx/appcompat/widget/N;
|
|
||||||
HSPLandroidx/appcompat/widget/N;-><init>(Landroidx/appcompat/widget/Toolbar;)V
|
|
||||||
HSPLandroidx/appcompat/widget/N;->run()V
|
|
||||||
Landroidx/fragment/app/m;
|
|
||||||
Lw/a;
|
|
||||||
HSPLandroidx/fragment/app/m;-><init>(Landroidx/fragment/app/q;)V
|
|
||||||
Landroidx/fragment/app/n;
|
|
||||||
HSPLandroidx/fragment/app/n;-><init>(Landroidx/fragment/app/q;)V
|
|
||||||
Landroidx/fragment/app/o;
|
|
||||||
HSPLandroidx/fragment/app/o;-><init>(Landroidx/fragment/app/q;)V
|
|
||||||
Landroidx/fragment/app/p;
|
|
||||||
HSPLandroidx/fragment/app/p;-><init>(Landroidx/fragment/app/q;)V
|
|
||||||
Landroidx/lifecycle/s;
|
|
||||||
HSPLandroidx/lifecycle/s;-><init>(Landroidx/lifecycle/t;)V
|
|
||||||
Lh/c;
|
|
||||||
HSPLh/c;->a(Ljava/lang/Object;)V
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 3,
|
|
||||||
"artifactType": {
|
|
||||||
"type": "COMPATIBLE_SCREEN_MANIFEST",
|
|
||||||
"kind": "Directory"
|
|
||||||
},
|
|
||||||
"applicationId": "io.truckwash.twa",
|
|
||||||
"variantName": "release",
|
|
||||||
"elements": []
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# This is a configuration file for ProGuard.
|
|
||||||
# http://proguard.sourceforge.net/index.html#manual/usage.html
|
|
||||||
#
|
|
||||||
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
|
|
||||||
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
|
|
||||||
# will be ignored by new version of the Android plugin for Gradle.
|
|
||||||
|
|
||||||
# Optimizations: If you don't want to optimize, use the proguard-android.txt configuration file
|
|
||||||
# instead of this one, which turns off the optimization flags.
|
|
||||||
-allowaccessmodification
|
|
||||||
|
|
||||||
# Preserve some attributes that may be required for reflection.
|
|
||||||
-keepattributes AnnotationDefault,
|
|
||||||
EnclosingMethod,
|
|
||||||
InnerClasses,
|
|
||||||
RuntimeVisibleAnnotations,
|
|
||||||
RuntimeVisibleParameterAnnotations,
|
|
||||||
RuntimeVisibleTypeAnnotations,
|
|
||||||
Signature
|
|
||||||
|
|
||||||
-keep public class com.google.vending.licensing.ILicensingService
|
|
||||||
-keep public class com.android.vending.licensing.ILicensingService
|
|
||||||
-keep public class com.google.android.vending.licensing.ILicensingService
|
|
||||||
-dontnote com.android.vending.licensing.ILicensingService
|
|
||||||
-dontnote com.google.vending.licensing.ILicensingService
|
|
||||||
-dontnote com.google.android.vending.licensing.ILicensingService
|
|
||||||
|
|
||||||
# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
|
|
||||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
|
||||||
native <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Keep setters in Views so that animations can still work.
|
|
||||||
-keepclassmembers public class * extends android.view.View {
|
|
||||||
void set*(***);
|
|
||||||
*** get*();
|
|
||||||
}
|
|
||||||
|
|
||||||
# We want to keep methods in Activity that could be used in the XML attribute onClick.
|
|
||||||
-keepclassmembers class * extends android.app.Activity {
|
|
||||||
public void *(android.view.View);
|
|
||||||
}
|
|
||||||
|
|
||||||
# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
|
|
||||||
-keepclassmembers enum * {
|
|
||||||
public static **[] values();
|
|
||||||
public static ** valueOf(java.lang.String);
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
|
||||||
public static final ** CREATOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Preserve annotated Javascript interface methods.
|
|
||||||
-keepclassmembers class * {
|
|
||||||
@android.webkit.JavascriptInterface <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
# The support libraries contains references to newer platform versions.
|
|
||||||
# Don't warn about those in case this app is linking against an older
|
|
||||||
# platform version. We know about them, and they are safe.
|
|
||||||
-dontnote android.support.**
|
|
||||||
-dontnote androidx.**
|
|
||||||
-dontwarn android.support.**
|
|
||||||
-dontwarn androidx.**
|
|
||||||
|
|
||||||
# Understand the @Keep support annotation.
|
|
||||||
-keep class android.support.annotation.Keep
|
|
||||||
|
|
||||||
-keep @android.support.annotation.Keep class * {*;}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@android.support.annotation.Keep <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@android.support.annotation.Keep <fields>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@android.support.annotation.Keep <init>(...);
|
|
||||||
}
|
|
||||||
|
|
||||||
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
|
|
||||||
-dontnote org.apache.http.**
|
|
||||||
-dontnote android.net.http.**
|
|
||||||
|
|
||||||
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
|
|
||||||
-dontnote java.lang.invoke.**
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# This is a configuration file for ProGuard.
|
|
||||||
# http://proguard.sourceforge.net/index.html#manual/usage.html
|
|
||||||
#
|
|
||||||
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
|
|
||||||
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
|
|
||||||
# will be ignored by new version of the Android plugin for Gradle.
|
|
||||||
|
|
||||||
# Optimization is turned off by default. Dex does not like code run
|
|
||||||
# through the ProGuard optimize steps (and performs some
|
|
||||||
# of these optimizations on its own).
|
|
||||||
# Note that if you want to enable optimization, you cannot just
|
|
||||||
# include optimization flags in your own project configuration file;
|
|
||||||
# instead you will need to point to the
|
|
||||||
# "proguard-android-optimize.txt" file instead of this one from your
|
|
||||||
# project.properties file.
|
|
||||||
-dontoptimize
|
|
||||||
|
|
||||||
# Preserve some attributes that may be required for reflection.
|
|
||||||
-keepattributes AnnotationDefault,
|
|
||||||
EnclosingMethod,
|
|
||||||
InnerClasses,
|
|
||||||
RuntimeVisibleAnnotations,
|
|
||||||
RuntimeVisibleParameterAnnotations,
|
|
||||||
RuntimeVisibleTypeAnnotations,
|
|
||||||
Signature
|
|
||||||
|
|
||||||
-keep public class com.google.vending.licensing.ILicensingService
|
|
||||||
-keep public class com.android.vending.licensing.ILicensingService
|
|
||||||
-keep public class com.google.android.vending.licensing.ILicensingService
|
|
||||||
-dontnote com.android.vending.licensing.ILicensingService
|
|
||||||
-dontnote com.google.vending.licensing.ILicensingService
|
|
||||||
-dontnote com.google.android.vending.licensing.ILicensingService
|
|
||||||
|
|
||||||
# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
|
|
||||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
|
||||||
native <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Keep setters in Views so that animations can still work.
|
|
||||||
-keepclassmembers public class * extends android.view.View {
|
|
||||||
void set*(***);
|
|
||||||
*** get*();
|
|
||||||
}
|
|
||||||
|
|
||||||
# We want to keep methods in Activity that could be used in the XML attribute onClick.
|
|
||||||
-keepclassmembers class * extends android.app.Activity {
|
|
||||||
public void *(android.view.View);
|
|
||||||
}
|
|
||||||
|
|
||||||
# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
|
|
||||||
-keepclassmembers enum * {
|
|
||||||
public static **[] values();
|
|
||||||
public static ** valueOf(java.lang.String);
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
|
||||||
public static final ** CREATOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Preserve annotated Javascript interface methods.
|
|
||||||
-keepclassmembers class * {
|
|
||||||
@android.webkit.JavascriptInterface <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
# The support libraries contains references to newer platform versions.
|
|
||||||
# Don't warn about those in case this app is linking against an older
|
|
||||||
# platform version. We know about them, and they are safe.
|
|
||||||
-dontnote android.support.**
|
|
||||||
-dontnote androidx.**
|
|
||||||
-dontwarn android.support.**
|
|
||||||
-dontwarn androidx.**
|
|
||||||
|
|
||||||
# Understand the @Keep support annotation.
|
|
||||||
-keep class android.support.annotation.Keep
|
|
||||||
|
|
||||||
-keep @android.support.annotation.Keep class * {*;}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@android.support.annotation.Keep <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@android.support.annotation.Keep <fields>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@android.support.annotation.Keep <init>(...);
|
|
||||||
}
|
|
||||||
|
|
||||||
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
|
|
||||||
-dontnote org.apache.http.**
|
|
||||||
-dontnote android.net.http.**
|
|
||||||
|
|
||||||
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
|
|
||||||
-dontnote java.lang.invoke.**
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# This is a configuration file for ProGuard.
|
|
||||||
# http://proguard.sourceforge.net/index.html#manual/usage.html
|
|
||||||
#
|
|
||||||
# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
|
|
||||||
# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
|
|
||||||
# will be ignored by new version of the Android plugin for Gradle.
|
|
||||||
|
|
||||||
# Optimizations can be turned on and off in the 'postProcessing' DSL block.
|
|
||||||
# The configuration below is applied if optimizations are enabled.
|
|
||||||
-allowaccessmodification
|
|
||||||
|
|
||||||
# Preserve some attributes that may be required for reflection.
|
|
||||||
-keepattributes AnnotationDefault,
|
|
||||||
EnclosingMethod,
|
|
||||||
InnerClasses,
|
|
||||||
RuntimeVisibleAnnotations,
|
|
||||||
RuntimeVisibleParameterAnnotations,
|
|
||||||
RuntimeVisibleTypeAnnotations,
|
|
||||||
Signature
|
|
||||||
|
|
||||||
-keep public class com.google.vending.licensing.ILicensingService
|
|
||||||
-keep public class com.android.vending.licensing.ILicensingService
|
|
||||||
-keep public class com.google.android.vending.licensing.ILicensingService
|
|
||||||
-dontnote com.android.vending.licensing.ILicensingService
|
|
||||||
-dontnote com.google.vending.licensing.ILicensingService
|
|
||||||
-dontnote com.google.android.vending.licensing.ILicensingService
|
|
||||||
|
|
||||||
# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
|
|
||||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
|
||||||
native <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Keep setters in Views so that animations can still work.
|
|
||||||
-keepclassmembers public class * extends android.view.View {
|
|
||||||
void set*(***);
|
|
||||||
*** get*();
|
|
||||||
}
|
|
||||||
|
|
||||||
# We want to keep methods in Activity that could be used in the XML attribute onClick.
|
|
||||||
-keepclassmembers class * extends android.app.Activity {
|
|
||||||
public void *(android.view.View);
|
|
||||||
}
|
|
||||||
|
|
||||||
# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
|
|
||||||
-keepclassmembers enum * {
|
|
||||||
public static **[] values();
|
|
||||||
public static ** valueOf(java.lang.String);
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
|
||||||
public static final ** CREATOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Preserve annotated Javascript interface methods.
|
|
||||||
-keepclassmembers class * {
|
|
||||||
@android.webkit.JavascriptInterface <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
# The support libraries contains references to newer platform versions.
|
|
||||||
# Don't warn about those in case this app is linking against an older
|
|
||||||
# platform version. We know about them, and they are safe.
|
|
||||||
-dontnote android.support.**
|
|
||||||
-dontnote androidx.**
|
|
||||||
-dontwarn android.support.**
|
|
||||||
-dontwarn androidx.**
|
|
||||||
|
|
||||||
# Understand the @Keep support annotation.
|
|
||||||
-keep class android.support.annotation.Keep
|
|
||||||
|
|
||||||
-keep @android.support.annotation.Keep class * {*;}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@android.support.annotation.Keep <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@android.support.annotation.Keep <fields>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembers class * {
|
|
||||||
@android.support.annotation.Keep <init>(...);
|
|
||||||
}
|
|
||||||
|
|
||||||
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
|
|
||||||
-dontnote org.apache.http.**
|
|
||||||
-dontnote android.net.http.**
|
|
||||||
|
|
||||||
# These classes are duplicated between android.jar and core-lambda-stubs.jar.
|
|
||||||
-dontnote java.lang.invoke.**
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
31=0/.dm
|
|
||||||
2147483647=0/.dm
|
|
||||||
28=1/.dm
|
|
||||||
29=1/.dm
|
|
||||||
30=1/.dm
|
|
||||||