From 3378ef9a5a9c471370d4ab7ae4dbfe1899df4dcd Mon Sep 17 00:00:00 2001 From: Mizan Ali Date: Wed, 14 Oct 2026 22:42:48 +0400 Subject: [PATCH 1/4] ci: build native apps only for apps a pr changes pushes to main and prs touching anything outside apps/ still build every app. the matrix now comes from apps/, so new apps need no ci edit, and a single native job rolls up the builds for branch protection. --- .github/workflows/ci.yml | 69 +++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9984ffc..844755a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,14 +42,41 @@ jobs: done done + # Native builds only for apps a PR touches. Pushes to main, and PRs that touch + # anything outside apps/ (lockfile, packages/, CI), build every app. + changes: + runs-on: ubuntu-latest + outputs: + apps: ${{ steps.apps.outputs.apps }} + steps: + - uses: actions/checkout@v7 + with: + # The PR merge commit plus its parents, so HEAD^1..HEAD is the PR's diff. + fetch-depth: 2 + - id: apps + run: | + all=$(ls apps | jq -Rsc 'split("\n") | map(select(length > 0))') + apps=$all + if [ "${{ github.event_name }}" = pull_request ]; then + files=$(git diff --name-only HEAD^1 HEAD | grep -vE '^docs/|\.(md|gif|png|jpe?g)$' || true) + if [ -z "$files" ]; then + apps='[]' + elif ! grep -qv '^apps/' <<<"$files"; then + apps=$(grep '^apps/' <<<"$files" | cut -d/ -f2 \ + | jq -Rsc --argjson all "$all" 'split("\n") | unique | map(select(IN($all[])))') + fi + fi + echo "apps=$apps" | tee -a "$GITHUB_OUTPUT" + android: - needs: check + needs: [check, changes] + if: needs.changes.outputs.apps != '[]' runs-on: ubuntu-latest timeout-minutes: 45 strategy: fail-fast: false matrix: - app: [tictactogether, stocksync, orderup, cowrite, outagenet, yardgate, eventfloor, agrimesh, chargecatch, surfrroll] + app: ${{ fromJSON(needs.changes.outputs.apps) }} steps: - uses: actions/checkout@v7 - uses: pnpm/action-setup@v6 @@ -88,33 +115,14 @@ jobs: exit 1 ios: - needs: check + needs: [check, changes] + if: needs.changes.outputs.apps != '[]' runs-on: macos-15 timeout-minutes: 60 strategy: fail-fast: false matrix: - include: - - app: tictactogether - scheme: Tictactogether - - app: stocksync - scheme: StockSync - - app: orderup - scheme: OrderUp - - app: cowrite - scheme: Cowrite - - app: outagenet - scheme: OutageNet - - app: yardgate - scheme: YardGate - - app: eventfloor - scheme: EventFloor - - app: agrimesh - scheme: AgriMesh - - app: chargecatch - scheme: ChargeCatch - - app: surfrroll - scheme: SurfrRoll + app: ${{ fromJSON(needs.changes.outputs.apps) }} steps: - uses: actions/checkout@v7 - uses: pnpm/action-setup@v6 @@ -151,6 +159,17 @@ jobs: - name: Build for the iOS simulator working-directory: apps/${{ matrix.app }}/ios run: | - xcodebuild -workspace "${{ matrix.scheme }}.xcworkspace" -scheme "${{ matrix.scheme }}" \ + scheme=$(basename *.xcworkspace .xcworkspace) + xcodebuild -workspace "$scheme.xcworkspace" -scheme "$scheme" \ -configuration Debug -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' \ -derivedDataPath "$RUNNER_TEMP/dd" CODE_SIGNING_ALLOWED=NO -quiet build + + # Single required check for the native builds, since the matrix varies per PR. + native: + if: always() + needs: [check, changes, android, ios] + runs-on: ubuntu-latest + steps: + - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: exit 1 + - run: echo "native builds passed or were not needed" From 0c275210576d18fe16db6204aa006204e2643264 Mon Sep 17 00:00:00 2001 From: Mizan Ali Date: Wed, 14 Oct 2026 23:02:43 +0400 Subject: [PATCH 2/4] ci: scope the js check to changed apps too typecheck, lint, test and the js bundle now run through turbo filters for the apps a pr touches; everything still runs when shared code changes. --- .github/workflows/ci.yml | 67 +++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 844755a..4fa5fa9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,38 +16,13 @@ env: TURBO_TELEMETRY_DISABLED: 1 jobs: - check: - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v7 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm typecheck - - run: pnpm lint - - run: pnpm test - - name: Bundle JavaScript for iOS and Android - run: | - for app in apps/*/; do - for platform in ios android; do - echo "::group::$app $platform" - (cd "$app" && npx react-native bundle --platform "$platform" --dev false \ - --entry-file index.js --bundle-output "$RUNNER_TEMP/$(basename "$app").$platform.jsbundle" \ - --assets-dest "$RUNNER_TEMP/assets-$(basename "$app")-$platform") - echo "::endgroup::" - done - done - - # Native builds only for apps a PR touches. Pushes to main, and PRs that touch - # anything outside apps/ (lockfile, packages/, CI), build every app. + # CI runs only for apps a PR touches. Pushes to main, and PRs that touch + # anything outside apps/ (lockfile, packages/, CI), check every app. changes: runs-on: ubuntu-latest outputs: apps: ${{ steps.apps.outputs.apps }} + filter: ${{ steps.apps.outputs.filter }} steps: - uses: actions/checkout@v7 with: @@ -67,6 +42,42 @@ jobs: fi fi echo "apps=$apps" | tee -a "$GITHUB_OUTPUT" + # No turbo filter when every app runs, so packages/ is checked too. + if [ "$apps" != "$all" ]; then + filter=$(jq -r 'map("--filter=@offline-app-examples/" + .) | join(" ")' <<<"$apps") + fi + echo "filter=${filter:-}" | tee -a "$GITHUB_OUTPUT" + + check: + needs: changes + if: needs.changes.outputs.apps != '[]' + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + APPS: ${{ needs.changes.outputs.apps }} + FILTER: ${{ needs.changes.outputs.filter }} + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm exec turbo typecheck $FILTER + - run: pnpm exec turbo lint $FILTER + - run: pnpm exec turbo test $FILTER + - name: Bundle JavaScript for iOS and Android + run: | + for app in $(jq -r '.[]' <<<"$APPS"); do + for platform in ios android; do + echo "::group::$app $platform" + (cd "apps/$app" && npx react-native bundle --platform "$platform" --dev false \ + --entry-file index.js --bundle-output "$RUNNER_TEMP/$app.$platform.jsbundle" \ + --assets-dest "$RUNNER_TEMP/assets-$app-$platform") + echo "::endgroup::" + done + done android: needs: [check, changes] From effb882863cec0f3e4c5ae911177a27136ca5300 Mon Sep 17 00:00:00 2001 From: Mizan Ali Date: Wed, 14 Oct 2026 23:03:20 +0400 Subject: [PATCH 3/4] ci: document how checks are scoped to changed apps --- .github/workflows/ci.yml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fa5fa9..60ce341 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,23 @@ +# CI for every app under apps/. +# +# Don't list apps in this file. The `changes` job reads apps/ and picks which +# apps to run: +# - PR touching only apps// -> just those apps +# - PR touching anything else (lockfile, packages/, this file, root config) +# -> every app +# - PR touching only docs (docs/, *.md, images) -> nothing +# - push to main -> every app +# +# `check` (typecheck, lint, test, JS bundle), `android` and `ios` all run for +# that list. A new app is picked up automatically, as long as: +# - apps//package.json is named @offline-app-examples/ +# - apps//ios has exactly one .xcworkspace whose scheme has +# the same name +# +# Branch protection should only require `check` and `native`, never per-app +# job names like `android (cowrite)`. Those only exist on PRs that touch that +# app. `native` rolls up all the android/ios jobs into one result. + name: CI on: @@ -16,8 +36,6 @@ env: TURBO_TELEMETRY_DISABLED: 1 jobs: - # CI runs only for apps a PR touches. Pushes to main, and PRs that touch - # anything outside apps/ (lockfile, packages/, CI), check every app. changes: runs-on: ubuntu-latest outputs: From f4155f701775625f6479af45bf31f2e9a326a386 Mon Sep 17 00:00:00 2001 From: Mizan Ali Date: Wed, 14 Oct 2026 23:03:34 +0400 Subject: [PATCH 4/4] docs: drop the ci matrix row step for new apps --- docs/DEMO_APPS_PLAN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DEMO_APPS_PLAN.md b/docs/DEMO_APPS_PLAN.md index afa42ac..24c421d 100644 --- a/docs/DEMO_APPS_PLAN.md +++ b/docs/DEMO_APPS_PLAN.md @@ -74,7 +74,7 @@ Planning only — no app scaffolds until implementation starts. | **A — Staff ops** | Event organiser | MLS staff group + OfflineID; dispatch over crowd relay (relay cannot read) | | **B — Gate sync** | Ticketing / gate POS | Shared **admission ledger** across scanners; idempotent mock platform ingest | -**Catalogue bar (Satvik / Mizan parity):** End-to-end RN app, seeded reset, README scripts, **`demo-staff.gif`** + **`demo-gate.gif`**, root README embeds, CI matrix row. +**Catalogue bar (Satvik / Mizan parity):** End-to-end RN app, seeded reset, README scripts, **`demo-staff.gif`** + **`demo-gate.gif`**, root README embeds (CI picks up new apps automatically; see the top of `.github/workflows/ci.yml`). **Flow A (short):** Security sends “Medic, Section B”; staff receiver gets it on **Bluetooth mesh** when cell is saturated; optional third phone relays.