diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 788095f..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,9 +36,44 @@ env: TURBO_TELEMETRY_DISABLED: 1 jobs: + changes: + runs-on: ubuntu-latest + outputs: + apps: ${{ steps.apps.outputs.apps }} + filter: ${{ steps.apps.outputs.filter }} + 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" + # 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 @@ -27,29 +82,30 @@ jobs: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - - run: pnpm typecheck - - run: pnpm lint - - run: pnpm test + - 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 apps/*/; do + for app in $(jq -r '.[]' <<<"$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") + (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 + 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, dockhand] + app: ${{ fromJSON(needs.changes.outputs.apps) }} steps: - uses: actions/checkout@v7 - uses: pnpm/action-setup@v6 @@ -88,35 +144,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: dockhand - scheme: DockHand + app: ${{ fromJSON(needs.changes.outputs.apps) }} steps: - uses: actions/checkout@v7 - uses: pnpm/action-setup@v6 @@ -153,6 +188,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" 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.