OverviewFilesCommitsIssuesDocsReleasesPeers

cthuloid commits / 2852fad

ci: build + sign the Android APK on android-v<x.y.z> tags only
.pear-git/ci.yaml: the forge runs CI for every ref, so a gate step decides and the others no-op unless it's a release tag; artifacts is the dist/ dir (APK + .sha256 on a tag, skipped.txt otherwise). Deps from lockfiles (npm ci), version from the tag, prebuild, gradle with the key from ~/android-toolchain/cthuloid.keystore (never in git), then verify the artifact: every native NEEDED entry present (verify-apk-addons) and the apksigner certificate equals the keystore's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2852fadb25e5649c3ccb7385197bf7eae14c44fb
by plankatron <fleeky@gmail.com> · 2026-10-02T16:01:33.000Z

Changed files (1) +96 -0

.pear-git/ci.yaml +96 -0
……@@ -0,0 +1,96 @@
1# pear-git CI for the cthuloid repo: builds the Android app (android/) on
2# android-v<x.y.z> TAGS ONLY, and publishes a signed APK as a forge download:
3# https://7rnx.net/git/releases/cthuloid/android-v<x.y.z>/dist/cthuloid-lights.apk
4# See docs/DESIGN-android.md §5.
5#
6# ⛔ The forge runs this for EVERY pushed branch and tag and cannot filter, so each
7# step checks the ref itself (GIT_REF / GIT_REF_KIND are injected) and does nothing
8# unless the gate step decided to build. Every push publishes `dist/` -- on a
9# non-release ref it holds only skipped.txt. (A listed-but-missing file would make
10# collectArtifacts throw and leave a misleading error in the forge log.)
11#
12# ⛔ CI runs in a `git archive | tar -x` extraction: no .git, no node_modules, no
13# PATH guarantees, `sh -c` (not bash). Host paths are absolute, as in
14# favela-surface-android's ci.yaml.
15#
16# ⛔ THE SIGNING KEY is ~/android-toolchain/cthuloid.keystore with its password in
17# cthuloid.keystore.pass -- outside the repo, never in git. Android refuses an
18# update signed with any other key (uninstall, i.e. data loss, is the only way
19# out), so the build FAILS if the key is missing rather than signing with another.
20
21steps:
22 - name: gate
23 run: |
24 set -e
25 mkdir -p dist
26 if [ "$GIT_REF_KIND" = "tag" ] && echo "$GIT_REF" | grep -Eq '^android-v[0-9]+\.[0-9]+\.[0-9]+$'; then
27 echo "release build: $GIT_REF"
28 else
29 echo "not an android-v<x.y.z> tag ($GIT_REF_KIND $GIT_REF): nothing to build"
30 echo "APKs are built on android-v<x.y.z> tags only; this ref ($GIT_REF_KIND $GIT_REF) built nothing." > dist/skipped.txt
31 touch .skip
32 fi
33 timeout: 30
34
35 - name: deps
36 run: |
37 set -e
38 [ -f .skip ] && exit 0
39 export PATH=/home/fleeky/.nvm/versions/node/v24.13.0/bin:$PATH
40 (cd webui && npm ci --no-audit --no-fund)
41 (cd android && npm ci --no-audit --no-fund)
42 test -e android/node_modules/cthuloid-web/lib/app.js
43 timeout: 900
44
45 - name: version
46 # The version comes from the tag, never from hand-editing a field.
47 run: |
48 set -e
49 [ -f .skip ] && exit 0
50 export PATH=/home/fleeky/.nvm/versions/node/v24.13.0/bin:$PATH
51 V=${GIT_REF#android-v}
52 (cd android && node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json'));p.version=process.argv[1];fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n')" "$V")
53 grep '"version"' android/package.json
54 timeout: 30
55
56 - name: prebuild
57 run: |
58 set -e
59 [ -f .skip ] && exit 0
60 export PATH=/home/fleeky/.nvm/versions/node/v24.13.0/bin:$PATH
61 (cd android && npm run prebuild:android)
62 test -s android/android/app/src/main/assets/worklet.bundle
63 timeout: 900
64
65 - name: assemble
66 run: |
67 set -e
68 [ -f .skip ] && exit 0
69 T=/home/fleeky/android-toolchain
70 test -s $T/cthuloid.keystore
71 test -s $T/cthuloid.keystore.pass
72 export JAVA_HOME=$T/jdk-17.0.13+11
73 export ANDROID_HOME=$T/sdk
74 export PATH=/home/fleeky/.nvm/versions/node/v24.13.0/bin:$PATH
75 export CTHULOID_KEYSTORE=$T/cthuloid.keystore
76 CTHULOID_KEYSTORE_PASS=$(cat $T/cthuloid.keystore.pass)
77 export CTHULOID_KEYSTORE_PASS
78 # Every new-architecture library's codegen must exist before CMake configure
79 # (favela-surface-android's ci.yaml); unqualified so it covers all of them.
80 $T/gradle-8.13/bin/gradle -p android/android generateCodegenArtifactsFromSchema
81 $T/gradle-8.13/bin/gradle -p android/android assembleRelease
82 cp android/android/app/build/outputs/apk/release/app-release.apk dist/cthuloid-lights.apk
83 # The artifact, not the intent: every native lib's NEEDED entries are in the APK...
84 (cd android && node scripts/verify-apk-addons.mjs ../dist/cthuloid-lights.apk)
85 # ...and it is signed with THIS key, not a debug or stray one.
86 WANT=$($JAVA_HOME/bin/keytool -list -v -keystore $T/cthuloid.keystore -storepass:file $T/cthuloid.keystore.pass | sed -n 's/.*SHA256: //p' | head -1 | tr -d ': ' | tr 'A-F' 'a-f')
87 GOT=$($ANDROID_HOME/build-tools/34.0.0/apksigner verify --print-certs dist/cthuloid-lights.apk | sed -n 's/.*certificate SHA-256 digest: //p' | head -1)
88 echo "signer want=$WANT got=$GOT"
89 [ -n "$WANT" ] && [ "$WANT" = "$GOT" ]
90 (cd dist && sha256sum cthuloid-lights.apk > cthuloid-lights.apk.sha256)
91 ls -l dist
92 timeout: 2700
93
94artifacts:
95 # A directory: files only on a release tag, skipped.txt otherwise.
96 - dist