OverviewFilesCommitsIssuesDocsReleasesPeers

favela-android / .pear-git / ci.yaml

188 lines · yaml ref:
# pear-git CI — build a signed, installable Android APK on push.
#
# Two different systems share the name "release" (same distinction hardops documents):
#
#   pgh publish .   -> pgh-sidecar's ReleaseRegistry. Content-addresses the tree for
#                      `pgh app install`. Metadata only — never a file.
#   this ci.yaml    -> the forge's build-release. Whatever is listed under `artifacts:`
#                      lands on /git/repos/favela-android/releases as a download.
#
# A phone downloads an .apk, so it comes from the second one. favela-android is NOT a pgh
# app (package.json says so) — there is no `pgh publish` step here.
#
# ⛔ CI runs in a `git archive | tar -x` extraction: no .git, no node_modules, and no PATH
# guarantees. Every host path below is absolute for that reason, following hyper-tps's
# ci.yaml — the other toolchain-heavy repo in this fleet (it drives a GPU Godot export the
# same way). Git metadata arrives via injected env vars, not `git rev-list`.
#
# Version comes from the ref: a tag is used verbatim, a branch becomes <branch>-<shortsha>.
# So a release named 0.0.1-test1 means pushing that TAG — never hand-editing a version field.

steps:
  - name: deps
    # The archive has no node_modules, and this closure cannot be resolved from the registry:
    # favela-web / hyper-proto-peer / atproto-identity are `file:../` siblings. So stage the
    # live checkout's tree, which is the documented pattern for toolchain-heavy repos.
    #
    # `cp -a` preserves the sibling symlinks, but they are RELATIVE (`../../favela-web`) and
    # resolve to nothing from a temp dir — so re-point them at absolute paths. Copying with -L
    # instead would dereference the whole tree and duplicate every sibling.
    #
    # ⛔ DERIVED, NOT A HARDCODED LIST. This was `for sib in favela-web hyper-proto-peer
    # atproto-identity`, and the day an import reached a FOURTH sibling the build broke:
    # `atproto-store` was linked in on 2026-08-09, the appview-ingestion commit made it
    # reachable from lib/node.js, and bare-pack died with `MODULE_NOT_FOUND: Cannot find module
    # 'atproto-store/lib/overlay-repo-store.js'` — a staging list that goes stale as the code
    # moves, which is the same failure this repo already fixed once for addons by reading the
    # bundle header instead of naming them. The symlinks themselves say which siblings exist,
    # so ask them.
    #
    # Then FAIL HERE on anything still dangling. Otherwise the first symptom is a bare-pack
    # module error two steps later, which reads like a source problem and is not.
    run: |
      set -e
      SRC=/storage/projects/code/hypercore-kb/hyper-fleeky
      cp -a "$SRC/favela-android/node_modules" .
      for link in node_modules/*; do
        target=$(readlink "$link" 2>/dev/null) || continue
        case "$target" in
          ../../*) ln -sfn "$SRC/$(basename "$target")" "$link" ;;
        esac
      done
      # `if ... fi`, NOT `[ -L "$l" ] && [ ! -e "$l" ] && echo`: under `set -e` the assignment
      # takes the exit status of the substitution, and a trailing false `&&` chain on the last
      # iteration is non-zero — so the healthy case aborted the step in 5.9s. An `if` with no
      # else is 0 when its condition is false.
      broken=$(for l in node_modules/*; do if [ -L "$l" ] && [ ! -e "$l" ]; then echo "$l"; fi; done)
      if [ -n "$broken" ]; then echo "dangling sibling symlink(s) after re-pointing:"; echo "$broken"; exit 1; fi
      test -e node_modules/favela-web/package.json
    timeout: 900

  - name: prebuild
    # Packs lib/node.js into assets/worklet.bundle, then stages exactly the addons that
    # bundle demands and fails if one has no prebuild (otherwise it surfaces as a dlopen
    # crash on a device, with no build-time signal). bare-pack is installed globally under
    # nvm, so PATH has to be set explicitly here.
    run: |
      set -e
      export PATH=/home/fleeky/.nvm/versions/node/v24.13.0/bin:$PATH
      npm run prebuild:android
      # Assert the path the APP ACTUALLY LOADS: app/worklet.js reads the Android asset
      # (RNFS.readFileAssets('worklet.bundle')), and pack:worklet + stage-addons.mjs both write
      # and read it there. This said `app/worklet.bundle` — the abandoned Metro-asset route,
      # which nothing has written since the bundle handoff changed — so it would have failed the
      # build even with the deps step fixed. An assertion pointed at a path nobody writes tests
      # the assertion, not the build.
      test -s android/app/src/main/assets/worklet.bundle
    timeout: 900

  - name: assemble
    # NDK 27 is not optional: RN 0.86.2's C++ uses std::format and NDK 26's libc++ has no
    # implementation of it. android/build.gradle pins 27.1.12297006.
    #
    # The keystore is durable and lives outside the repo — app/build.gradle defaults to
    # $HOME/android-toolchain/favela-spike.keystore, which resolves because CI runs as fleeky.
    # It is deliberately NOT in git.
    run: |
      set -e
      export JAVA_HOME=/home/fleeky/android-toolchain/jdk-17.0.13+11
      export ANDROID_HOME=/home/fleeky/android-toolchain/sdk
      export PATH=/home/fleeky/.nvm/versions/node/v24.13.0/bin:$PATH
      # Every new-architecture library's codegen dir is an input to CMake configure, and it lives in
      # THAT LIBRARY's build dir — a clean checkout has not generated it yet, so configure fails with
      # `add_subdirectory given source .../codegen/jni/ which is not an existing directory`, followed by
      # `Cannot specify link libraries for target react_codegen_<Name>Spec`.
      #
      # ⛔ UNQUALIFIED, so it runs in EVERY subproject that has the task. This named
      # `:react-native-bare-kit:` alone — the same going-stale-as-the-code-moves list the deps step above
      # was already fixed for — and it broke in exactly that way the day a SECOND such library arrived:
      # react-native-webview was added on 2026-08-11 for the WebView UI and assemble died in ninja.
      # Autolinking already knows which libraries exist; ask it rather than maintaining a copy.
      /home/fleeky/android-toolchain/gradle-8.13/bin/gradle -p android generateCodegenArtifactsFromSchema
      /home/fleeky/android-toolchain/gradle-8.13/bin/gradle -p android assembleRelease
      # The artifact path IS the public URL and the filename is what the user's browser saves, so
      # publish it under a name a person can recognise rather than the gradle output path.
      cp android/app/build/outputs/apk/release/app-release.apk favela-android.apk
      # The worklet bundle is published ALONGSIDE the APK so an installed phone can update its JS
      # without a 153 MB sideload — pgh-runtime fetches this file and verifies it against tree.json
      # (written by `npm run gen:tree` in the prebuild step). Native code still needs a new APK.
      cp android/app/src/main/assets/worklet.bundle worklet.bundle
      # The APK gets a tree too, and a SEPARATE one. pgh-runtime installs a TREE — it hashes what it is
      # handed and refuses bytes that do not reproduce the root hash — so until now the 154 MB APK was
      # the one artifact nothing verified, whoever fetched it. This is the precondition for ever fetching
      # it over the swarm instead of as a single unresumable HTTPS GET.
      #
      # ⛔ NOT merged into tree.json. A pgh tree holds many entries, so it would encode fine and would
      # brick every phone already in the field: the deployed bundleStore.materialize keeps the LAST file
      # entry it writes as the bundle to boot, so a two-entry tree stages a 154 MB APK as the worklet
      # bundle. The OTA client on a handset is exactly what cannot be fixed remotely.
      npm run gen:apk-tree
      ls -l favela-android.apk worklet.bundle tree.json apk.tree.json
    timeout: 2700

  # ── the worklet bundle, into the RELEASE REGISTRY, so a phone can fetch it P2P ─────────────────
  #
  # The `artifacts:` block below publishes files the forge serves over HTTPS at
  # /git/releases/<repo>/<version>/<file>. That is how the phone has always updated, and it is the
  # problem: 7rnx.net/git is LOAD-BALANCED, a release lands on the backend that built it and replicates
  # to the other asynchronously, and the phone asks whichever answers. Measured 2026-08-25 — 0.1.6
  # replicated in ~15s, 0.1.7 had NOT replicated after 600s and the phone correctly reported no update.
  # Measured before that on 2026-08-18, a 153 MB APK never reached the second backend at all.
  #
  # `pgh publish` is the other half, and the one every other hyper-fleeky app already uses: it
  # content-addresses the tree into the ReleaseRegistry, which a standalone device replicates over
  # HYPERSWARM by the publisher's registry key — no sidecar, no HTTPS, no backend to be on the wrong side
  # of. Proven end-to-end from a sidecar-free process before this step was written: resolve 13.1s cold,
  # getTree 6ms, getContent 19ms (pgh/lib/resolve-release.js path 2 + NetworkFetcher).
  #
  # ⛔ THE BUNDLE ONLY, NOT THE APK. The APK is 154 MB per build and the content store keeps every one;
  # `releases` was already 6.74 GiB with nothing scheduling gc (hyper-social #47 §2). Fetching the APK
  # P2P needs PackageInstaller + REQUEST_INSTALL_PACKAGES anyway (#54 §8) — apk.tree.json is published
  # for that and nothing consumes it yet, so putting 154 MB into the store now would buy nothing.
  #
  # ⛔ THIS TREE HAS TWO ENTRIES (package.json + worklet.bundle), because pgh publish requires a manifest.
  # The bundle must therefore be selected BY NAME on the phone, never by position — see the note in the
  # assemble step above about materialize keeping the LAST file entry, which is exactly the trap that
  # would otherwise stage a manifest as the worklet bundle.
  - name: pgh publish (worklet bundle → release registry, for P2P OTA)
    run: |
      set -e
      rm -rf .pgh-publish && mkdir -p .pgh-publish
      cp worklet.bundle .pgh-publish/worklet.bundle
      # Version comes from the repo's package.json — the same field pgh publish reads for every other
      # app, so a release is identified the way the rest of the fleet identifies one.
      VERSION=$(node -p "require('./package.json').version")
      cat > .pgh-publish/package.json <<JSON
      {
        "name": "favela-android",
        "version": "$VERSION",
        "pgh": {
          "upgrade": "pgh://1dd36bdfa9cb7f7d84adbd07f017e7200f7f90a3a546cd0761fac2a1f8a13e86/favela-android",
          "runtime": "bare"
        }
      }
      JSON
      cat .pgh-publish/package.json
      # --accept-conflict: republishing one version with different content is EXPECTED here, because a
      # branch rebuild re-runs without a version bump. Same reason hyper-proto-peer's publish carries it.
      pgh publish .pgh-publish --accept-conflict "automated CI publish of the favela-android worklet bundle; version tracks package.json and a branch rebuild republishes it"
      # Prove the registry actually has it, in the build log, rather than assuming the publish landed.
      pgh releases latest favela-android || true
    timeout: 300

artifacts:
  # ⛔ FILES ONLY — collectArtifacts uses readFile, with no directory walk and no globbing.
  - favela-android.apk
  # The OTA pair. tree.json is what makes the bundle FETCHABLE at all: pgh-runtime installs a TREE, and
  # hashes the entries it is handed against the root hash before it will accept them — so a release with
  # a bundle and no manifest is unfetchable by design, which is the state this repo shipped in.
  #
  # ⚠ ~37 MB per release on top of the 153 MB APK. `releases` was already 6.74 GiB with gc({keepN:3})
  # existing and nothing scheduling it (hyper-social #47 §2); this makes that trajectory steeper.
  - worklet.bundle
  - tree.json
  # The APK's manifest — 289 bytes, and the only thing that makes that 154 MB fetchable-and-verifiable
  # by anything other than a browser. Nothing consumes it yet; publishing it first is what lets a
  # swarm-side fetcher be written without another build round-trip.
  - apk.tree.json