Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 80 additions & 34 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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/<name>/ -> 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/<name>/package.json is named @offline-app-examples/<name>
# - apps/<name>/ios has exactly one <Scheme>.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:
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
2 changes: 1 addition & 1 deletion docs/DEMO_APPS_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading