6 Commits

Author SHA1 Message Date
c1cbc02826 fix: improve iOS CI workflow with pipefail, fix Python indentation, add setup docs
- Add set -o pipefail for proper build error detection
- Replace PIPESTATUS with $? (works correctly with pipefail)
- Fix Python indentation in inline scripts (avoids YAML linter false positives)
- Remove MergeTitle from Gitea merge API call (uses default)
- Add docs/ci-cd-setup.md with Mac runner and secrets setup guide
2026-06-26 20:42:58 +00:00
87e0610a62 ci: add PR → iPhone → LGTM workflow
- Triggers on PR to main (opened, synchronize, reopened)
- Build-deploy job: xcodegen + xcodebuild for iOS device
- Posts PR comment when build succeeds
- wait-approval job: polls for LGTM/KO comments via Gitea API
- Auto-merges on LGTM, blocks on KO, timeout after 2h
2026-06-26 20:41:14 +00:00
1ca8c3a467 Revert "ci: add PR → iPhone → LGTM workflow"
All checks were successful
CI / Detect Changes (push) Successful in 4s
CI / Admin Web CI (push) Has been skipped
CI / YouTube Worker (push) Has been skipped
CI / Deploy (push) Has been skipped
This reverts commit 8116b18a38.
2026-06-26 20:39:12 +00:00
8116b18a38 ci: add PR → iPhone → LGTM workflow 2026-06-26 20:36:51 +00:00
bd81ce92ad docs: add AGENTS.md for Hermes agent context
All checks were successful
CI / Detect Changes (push) Successful in 3s
CI / Admin Web CI (push) Has been skipped
CI / YouTube Worker (push) Has been skipped
CI / Deploy (push) Has been skipped
2026-06-26 18:30:17 +00:00
77c17046d5 Merge pull request 'feat: move HealthKit permission to onboarding, remove HR write' (#3) from fix/healthkit-setup-popup into main
All checks were successful
CI / Detect Changes (push) Successful in 3s
CI / Admin Web CI (push) Has been skipped
CI / YouTube Worker (push) Has been skipped
CI / Deploy (push) Has been skipped
Reviewed-on: #3
2026-05-24 15:29:12 +02:00
3 changed files with 655 additions and 471 deletions

View File

@@ -0,0 +1,239 @@
# =============================================================================
# TabataGo — PR → Build → iPhone → Wait LGTM → Merge
# =============================================================================
# Chaque PR déclenche ce workflow :
# 1. Build l'app pour device iOS (iPhone physique)
# 2. Poste un commentaire sur la PR
# 3. Attend un commentaire "LGTM" de Millian
# 4. Merge automatique
# =============================================================================
name: PR → iPhone → LGTM
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
# ── Permissions ──────────────────────────────────────────────────────────────
permissions:
contents: write
pull-requests: write
# ── Concurrency ──────────────────────────────────────────────────────────────
# Cancel any in-progress runs for the same PR when a new push arrives
concurrency:
group: pr-iphone-${{ github.event.pull_request.number }}
cancel-in-progress: true
# ── Jobs ─────────────────────────────────────────────────────────────────────
jobs:
# ─── Build + Deploy ────────────────────────────────────────────────────────
build-deploy:
name: Build & Install on iPhone
runs-on: macos
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Xcode
run: |
sudo xcode-select -s /Applications/Xcode.app
echo "Using Xcode $(xcodebuild -version | head -1)"
- name: Install XcodeGen (if needed)
run: |
if ! command -v xcodegen &> /dev/null; then
brew install xcodegen
fi
- name: Generate Xcode project
run: |
cd tabatago-swift
xcodegen generate
- name: Build for iPhone
env:
IPHONE_UDID: ${{ secrets.IPHONE_UDID }}
run: |
cd tabatago-swift
# Build for device (build only — installation requires USB/OTA tooling)
# Use tee to capture full log while showing last 40 lines on stdout
set -o pipefail
xcodebuild build \
-scheme TabataGo \
-sdk iphoneos \
-destination "platform=iOS,id=${IPHONE_UDID}" \
-configuration Debug \
-allowProvisioningUpdates \
-derivedDataPath ../build \
-scmProvider system \
CODE_SIGN_STYLE=Automatic \
2>&1 | tee ../build/xcodebuild.log | tail -40
BUILD_EXIT=$?
if [ $BUILD_EXIT -ne 0 ]; then
echo ""
echo "❌ Build FAILED (exit code: $BUILD_EXIT)"
echo "Full log: build/xcodebuild.log"
exit $BUILD_EXIT
fi
echo ""
echo "✅ App built successfully for iPhone (UDID: ${IPHONE_UDID})"
- name: Post "Ready to test" comment
if: success()
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_HOST: https://gitea.1000co.fr
run: |
PR="${{ github.event.pull_request.number }}"
REPO="${{ github.repository }}"
RESP=$(curl -sk -w "\n%{http_code}" -X POST \
-H "Authorization: token $GITEA_TOKEN \
-H "Content-Type: application/json" \
-d '{"body":"## 📱 Prêt à tester !\n\nL'"'"'app a été buildée pour ton iPhone.\n\n- Teste les changements\n- Reply **LGTM** pour merger\n- Reply **KO** pour bloquer"}' \
"${GITEA_HOST}/api/v1/repos/${REPO}/issues/${PR}/comments")
HTTP_CODE=$(echo "$RESP" | tail -1)
BODY=$(echo "$RESP" | sed '$d')
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✅ Comment posted (HTTP $HTTP_CODE)"
else
echo "⚠️ Failed to post comment (HTTP $HTTP_CODE):"
echo "$BODY"
fi
# ─── Wait for LGTM ─────────────────────────────────────────────────────────
wait-approval:
name: Wait for LGTM comment
needs: build-deploy
runs-on: macos
timeout-minutes: 120 # 2 heures max
steps:
- name: Poll for LGTM
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_HOST: https://gitea.1000co.fr
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
REPO="${{ github.repository }}"
API="${GITEA_HOST}/api/v1/repos/${REPO}"
MAX_TRIES=240 # 2h = 240 × 30s
TRIES=0
echo "⏳ Attente du commentaire LGTM sur la PR #${PR_NUMBER}..."
echo " Reply 'LGTM' sur la PR pour merger automatiquement."
echo " Reply 'KO' pour bloquer."
echo ""
# Récupérer le dernier ID de commentaire existant comme point de départ
LAST_ID=$(curl -sk \
-H "Authorization: token $GITEA_TOKEN \
"${API}/issues/${PR_NUMBER}/comments" | \
python3 -c "import json,sys; cs=json.load(sys.stdin); print(cs[-1]['id'] if isinstance(cs,list) and cs else 0)" 2>/dev/null || echo 0)
if [ "$LAST_ID" = "0" ] || [ -z "$LAST_ID" ]; then
echo " Aucun commentaire existant. En attente du premier..."
fi
while [ $TRIES -lt $MAX_TRIES ]; do
sleep 30
TRIES=$((TRIES + 1))
# Récupérer tous les commentaires
COMMENTS=$(curl -sk \
-H "Authorization: token $GITEA_TOKEN \
"${API}/issues/${PR_NUMBER}/comments" 2>/dev/null)
if [ -z "$COMMENTS" ]; then
continue
fi
# Vérifier les nouveaux commentaires (ID > LAST_ID)
NEW_COMMENTS=$(echo "$COMMENTS" | python3 -c "
import json, sys
last_id = $LAST_ID
cs = json.load(sys.stdin)
new = [c for c in cs if isinstance(c, dict) and c.get('id', 0) > last_id]
print(json.dumps(new))
" 2>/dev/null)
if [ -z "$NEW_COMMENTS" ] || [ "$NEW_COMMENTS" = "[]" ]; then
# Petit heartbeat toutes les 2 min
if [ $((TRIES % 4)) -eq 0 ]; then
echo " ⏳ Toujours en attente... (${TRIES}/240, $(date +%H:%M:%S))"
fi
continue
fi
# Mettre à jour LAST_ID au plus récent
LAST_ID=$(echo "$COMMENTS" | python3 -c "
import json, sys
cs = json.load(sys.stdin)
ids = [c['id'] for c in cs if isinstance(c, dict)]
print(max(ids) if ids else 0)
" 2>/dev/null || echo "$LAST_ID")
# Vérifier LGTM (insensible à la casse, dans le texte du commentaire)
if echo "$NEW_COMMENTS" | python3 -c "
import json, sys
cs = json.load(sys.stdin)
for c in cs:
body = c.get('body', '')
if 'lgtm' in body.strip().lower():
print('MATCH')
break
" 2>/dev/null | grep -q MATCH; then
echo ""
echo "✅ LGTM reçu ! Merge de la PR..."
MERGE_RESP=$(curl -sk -w "\n%{http_code}" -X POST \
-H "Authorization: token $GITEA_TOKEN \
-H "Content-Type: application/json" \
-d "{\"Do\":\"merge\"}" \
"${API}/pulls/${PR_NUMBER}/merge")
MERGE_HTTP=$(echo "$MERGE_RESP" | tail -1)
if [ "$MERGE_HTTP" -ge 200 ] && [ "$MERGE_HTTP" -lt 300 ]; then
echo "✅ PR mergée avec succès (HTTP $MERGE_HTTP)."
exit 0
else
echo "⚠️ Merge API returned HTTP $MERGE_HTTP"
echo "$MERGE_RESP" | sed '$d'
# Ne pas échouer le workflow — le merge a peut-être fonctionné
exit 0
fi
fi
# Vérifier KO (insensible à la casse)
if echo "$NEW_COMMENTS" | python3 -c "
import json, sys
cs = json.load(sys.stdin)
for c in cs:
body = c.get('body', '')
if body.strip().lower() == 'ko':
print('MATCH')
break
" 2>/dev/null | grep -q MATCH; then
echo ""
echo "❌ KO reçu. PR bloquée."
exit 1
fi
# Heartbeat
if [ $((TRIES % 4)) -eq 0 ]; then
echo " ⏳ Toujours en attente... (${TRIES}/240, $(date +%H:%M:%S))"
fi
done
echo ""
echo "❌ Timeout — pas de LGTM reçu après 2 heures."
exit 1

593
AGENTS.md
View File

@@ -1,491 +1,142 @@
# AGENTS.md
# TabataGo — iOS Tabata Workout App
This file contains optimizations and best practices for working with this TabataFit Expo project.
> Tabata interval training app for iOS + watchOS with HealthKit integration.
---
## Agent Context
## 🚀 Command Optimizations
This project is part of the **Millian Team** — an AI agent engineering team.
When working here, agents should load the relevant skill:
- `senior-ios` — SwiftUI, Xcode, iOS/watchOS development
- `senior-backend` — Supabase backend
- `senior-devops` — Deploy infrastructure
- `po-pm` — Product vision, specs, validation
Use the following `rtk` commands for optimal performance:
## Tech Stack
| Command | rtk Equivalent | Speedup | Tokens Before | Tokens After | Savings |
|---------|----------------|---------|---------------|--------------|---------|
| `ls` / `tree` | `rtk ls` | 10x | 2,000 | 400 | -80% |
| `cat` / `read` | `rtk read` | 20x | 40,000 | 12,000 | -70% |
| `grep` / `rg` | `rtk grep` | 8x | 16,000 | 3,200 | -80% |
| `git status` | `rtk git status` | 10x | 3,000 | 600 | -80% |
| `git diff` | `rtk git diff` | 5x | 10,000 | 2,500 | -75% |
| `git log` | `rtk git log` | 5x | 2,500 | 500 | -80% |
| `git add/commit/push` | `rtk git` | 8x | 1,600 | 120 | -92% |
| `cargo test` / `npm test` | `rtk test` | 5x | 25,000 | 2,500 | -90% |
| `ruff check` | `rtk lint` | 3x | 3,000 | 600 | -80% |
| `pytest` | `rtk pytest` | 4x | 8,000 | 800 | -90% |
| `go test` | `rtk gotest` | 3x | 6,000 | 600 | -90% |
| `docker ps` | `rtk docker` | - | - | - | - |
| Layer | Tech |
|-------|------|
| App | SwiftUI, iOS 26.0+ |
| Watch | SwiftUI, watchOS 11.0+ |
| Widget | WidgetKit, Live Activities |
| Language | Swift 6.0 (strict concurrency) |
| Architecture | MVVM, @Observable, AppState central |
| Backend | Supabase (PostgreSQL, Auth, Storage) |
| Subscriptions | RevenueCat 5.x |
| Analytics | PostHog 3.x |
| Health | HealthKit (heart rate, calories, workouts) |
| Music | Apple Music integration + YouTube worker |
| Build | XcodeGen (project.yml → .xcodeproj) |
| Package Manager | Swift Package Manager |
### Usage Examples
## Project Structure
```
tabatago/
├── tabatago-swift/ # Main Xcode project
│ ├── project.yml # XcodeGen spec
│ ├── Config/
│ │ ├── Secrets.xcconfig # API keys (not committed)
│ │ └── Secrets.xcconfig.example
│ ├── TabataGo/ # iOS App
│ │ ├── App/ # App entry, state, root view
│ │ │ ├── TabataGoApp.swift
│ │ │ ├── RootView.swift
│ │ │ └── AppState.swift
│ │ ├── Views/
│ │ │ ├── Tabs/ # MainTabView, Home, Activity, Programs, Profile
│ │ │ ├── Player/ # Workout player (PlayerView, NowPlaying)
│ │ │ ├── Programs/ # ProgramDetail, BodyZone
│ │ │ ├── Onboarding/ # Onboarding flow
│ │ │ ├── Paywall/ # RevenueCat paywall
│ │ │ ├── Complete/ # Workout completion screen
│ │ │ ├── Settings/ # Settings, policies
│ │ │ └── Components/ # Shared UI components
│ │ ├── ViewModels/ # HomeVM, HealthVM, PlayerVM, PurchaseVM, MusicVM
│ │ ├── Models/ # WorkoutProgram, WorkoutSession, UserProfile, etc.
│ │ ├── Services/ # Supabase, HealthKit, Music, Audio, Purchase, Analytics
│ │ ├── Theme/ # App theme
│ │ ├── Utilities/ # Strings, Environment
│ │ └── Resources/ # Assets, Localizable, entitlements
│ ├── TabataGoWatch/ # watchOS App
│ │ ├── Views/ # WatchRoot, WatchIdle, WatchActivity, WatchPlayer
│ │ ├── Services/ # WatchConnectivity, WatchPlayerEngine
│ │ ├── Complications/ # Watch face complications
│ │ └── Resources/
│ ├── TabataGoWidget/ # iOS Widget + Live Activities
│ ├── TabataGoTests/ # Unit tests
│ └── TabataGoUITests/ # UI tests
├── supabase/ # Supabase config
│ ├── schema.sql
│ ├── seed.sql
│ ├── setup-admin.sql
│ └── migrations/ # SQL migrations (001-006)
└── youtube-worker/ # Node.js worker for YouTube music
├── server.js
├── package.json
└── Dockerfile
```
## Dependencies (SPM)
- **Supabase** 2.5+ — Backend SDK (Auth, Database, Storage)
- **RevenueCat** 5.0+ — In-app purchases
- **PostHog** 3.0+ — Analytics
## Features
- Tabata/interval workout player with configurable timers
- Pre-built workout programs by body zone
- HealthKit integration (heart rate, calories, workout saving)
- Apple Watch companion with real-time metrics
- Live Activities + Dynamic Island
- Music integration (Apple Music + YouTube)
- RevenueCat subscriptions
- Onboarding flow
- Activity tracking & workout history
## Commands
```bash
# Instead of: ls -la src/
rtk ls src/
# From tabatago-swift/
xcodegen generate # Generate .xcodeproj
open TabataGo.xcodeproj # Open in Xcode
# Instead of: cat package.json
rtk read package.json
# Instead of: grep -r "useEffect" src/
rtk grep "useEffect" src/
# Instead of: git status
rtk git status
# Instead of: npm test
rtk test
# Instead of: ruff check .
rtk lint
# In Xcode
Cmd+R → Run on simulator
Cmd+U → Run tests
```
---
## Configuration
## 📱 Expo Best Practices
- `Config/Secrets.xcconfig` contains API keys (not committed)
- Template: `Config/Secrets.xcconfig.example`
- Env vars: `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `REVENUECAT_API_KEY`, `POSTHOG_API_KEY`
### Development Workflow
## Current State
**CRITICAL: Always try Expo Go first before creating custom builds.**
- **Version**: 1.0 (build 2) — not yet released
- **State**: App functional, needs finishing work
- **Platforms**: iOS 26.0+, watchOS 11.0+
- **Requires**: macOS with Xcode 26+ to build
1. **Start with Expo Go**: Run `npx expo start` and scan the QR code with Expo Go
2. **Check if features work**: Test your app thoroughly in Expo Go
3. **Only create custom builds when required**
## Supabase Schema
#### When Custom Builds Are Required
Tables: `profiles`, `workout_sessions`, `workout_programs`, `download_jobs`, `music_genre`, etc.
See `supabase/schema.sql` and `supabase/migrations/` for full schema.
Use `npx expo run:ios/android` or `eas build` ONLY when using:
- **Local Expo modules** (custom native code in `modules/`)
- **Apple targets** (widgets, app clips, extensions via `@bacons/apple-targets`)
- **Third-party native modules** not included in Expo Go
- **Custom native configuration** that can't be expressed in `app.json`
## Team Rules
#### When Expo Go Works
1. **Swift Concurrency** — async/await everywhere, no completion handlers
2. **@Observable** — not @ObservableObject
3. **No force unwrap**`guard let` always, never `!`
4. **SwiftUI only** — no UIKit unless unavoidable
5. **XcodeGen** — project.yml is source of truth, not .xcodeproj
6. **Secrets** — never commit Secrets.xcconfig
7. **French** — all user-facing strings in French (Localizable.xcstrings)
Expo Go supports a huge range of features out of the box:
- All `expo-*` packages (camera, location, notifications, etc.)
- Expo Router navigation
- Most UI libraries (reanimated, gesture handler, etc.)
- Push notifications, deep links, and more
## Project Context
### Common Commands
```bash
# Development
npx expo start # Start development server
npx expo start --tunnel # If network issues
npx expo start --clear # Clear cache
npx tsc --noEmit # Type check
npx expo install <pkg> # Install Expo-compatible packages
# Building
npx eas-cli@latest build --profile development # Dev build
npx eas-cli@latest build -p ios --profile production --submit # Build and submit iOS
npx testflight # Quick TestFlight shortcut
# Development Client (when native code changes)
npx expo start --dev-client
```
### Code Style Rules
#### File Naming
- Use kebab-case for file names (e.g., `workout-card.tsx`)
- Never use special characters in file names
- Always remove old route files when moving or restructuring navigation
#### Imports
- Use path aliases from `tsconfig.json` instead of relative imports
- Prefer aliases over relative imports for refactors
- Always use import statements at the top of the file
#### Library Preferences
-`expo-audio` not `expo-av`
-`expo-video` not `expo-av`
-`expo-image` with `source="sf:name"` for SF Symbols
-`react-native-safe-area-context` not react-native SafeAreaView
-`process.env.EXPO_OS` not `Platform.OS`
-`React.use` not `React.useContext`
- ❌ Never use modules removed from React Native: Picker, WebView, SafeAreaView, AsyncStorage
- ❌ Never use legacy expo-permissions
- ❌ Never use intrinsic elements like 'img' or 'div' unless in webview
### Route Structure
```
app/
_layout.tsx # Root layout with tabs
(tabs)/ # Group routes for tabs
_layout.tsx # Tabs layout
index.tsx # Home tab
workouts.tsx # Workouts tab
activity.tsx # Activity tab
browse.tsx # Browse tab
profile.tsx # Profile tab
player/
[id].tsx # Workout player
```
#### Route Rules
- Routes belong in the `app` directory
- Never co-locate components, types, or utilities in the app directory
- Ensure the app always has a route that matches "/"
- Always use `_layout.tsx` files to define stacks
### UI Guidelines
#### Responsiveness
- Always wrap root component in a scroll view for responsiveness
- Use `<ScrollView contentInsetAdjustmentBehavior="automatic" />` instead of `<SafeAreaView>`
- `contentInsetAdjustmentBehavior="automatic"` should be applied to FlatList and SectionList
- Use flexbox instead of Dimensions API
- ALWAYS prefer `useWindowDimensions` over `Dimensions.get()`
#### Styling
- Prefer flex gap over margin and padding styles
- Prefer padding over margin where possible
- Inline styles not StyleSheet.create unless reusing styles is faster
- Use `{ borderCurve: 'continuous' }` for rounded corners
- ALWAYS use a navigation stack title instead of a custom text element
- When padding a ScrollView, use `contentContainerStyle` padding
- Add entering and exiting animations for state changes
#### Shadows
Use CSS `boxShadow` style prop. NEVER use legacy React Native shadow or elevation styles.
```tsx
<View style={{ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }} />
```
#### Text Styling
- Add the `selectable` prop to every `<Text/>` element displaying important data
- Counters should use `{ fontVariant: 'tabular-nums' }` for alignment
#### Safe Area
- Always account for safe area with stack headers, tabs, or ScrollView/FlatList `contentInsetAdjustmentBehavior="automatic"`
- Ensure both top and bottom safe area insets are accounted for
### Navigation Patterns
#### Stack Configuration
```tsx
import { Stack } from 'expo-router/stack';
<Stack
screenOptions={{
headerTransparent: true,
headerShadowVisible: false,
headerLargeTitleShadowVisible: false,
headerLargeStyle: { backgroundColor: "transparent" },
headerTitleStyle: { color: PlatformColor("label") },
headerLargeTitle: true,
headerBlurEffect: "none",
headerBackButtonDisplayMode: "minimal",
}}
>
<Stack.Screen name="index" options={{ title: "Home" }} />
</Stack>
```
#### Links with Previews
```tsx
import { Link } from 'expo-router';
<Link href="/settings">
<Link.Trigger>
<Pressable>
<Card />
</Pressable>
</Link.Trigger>
<Link.Preview />
<Link.Menu>
<Link.MenuAction title="Share" icon="square.and.arrow.up" />
<Link.MenuAction title="Delete" icon="trash" destructive />
</Link.Menu>
</Link>
```
#### Modal/Sheet Presentations
```tsx
// Modal
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
// Form Sheet
<Stack.Screen
name="sheet"
options={{
presentation: "formSheet",
sheetGrabberVisible: true,
sheetAllowedDetents: [0.5, 1.0],
contentStyle: { backgroundColor: "transparent" },
}}
/>
```
### Data Fetching
#### Environment Variables
- Use `EXPO_PUBLIC_` prefix for client-side environment variables
- Never put secrets in `EXPO_PUBLIC_` variables (visible in built app)
- Restart the dev server after changing `.env` files
```bash
# .env
EXPO_PUBLIC_API_URL=https://api.example.com
EXPO_PUBLIC_SUPABASE_URL=your-supabase-url
```
#### API Client Pattern
```tsx
const BASE_URL = process.env.EXPO_PUBLIC_API_URL;
export const apiClient = {
get: async <T,>(path: string): Promise<T> => {
const response = await fetch(`${BASE_URL}${path}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
};
```
#### React Query Setup
```tsx
// app/_layout.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 2,
},
},
});
```
### EAS Build Configuration
```json
{
"cli": {
"version": ">= 16.0.1",
"appVersionSource": "remote"
},
"build": {
"production": {
"autoIncrement": true,
"ios": {
"resourceClass": "m-medium"
}
},
"development": {
"developmentClient": true,
"distribution": "internal"
}
},
"submit": {
"production": {}
}
}
```
### Testing & Type Checking
```bash
# TypeScript
npx tsc --noEmit
# Run unit tests (Vitest)
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage report
npm run test:coverage
# Run Maestro E2E tests
npm run test:maestro
# Lint
npx eslint .
```
#### Test Structure
```
src/__tests__/
setup.ts # Mocks and test configuration
stores/ # Zustand store tests
hooks/ # React hooks tests
services/ # Service layer tests
components/ # Component logic tests
data/ # Data validation tests
```
#### Coverage Goals
- **Stores**: 80%+ (business logic)
- **Services**: 80%+ (API integration)
- **Hooks**: 70%+ (timer, purchases)
- **Components**: 50%+ (critical UI)
### Key Takeaways
1. **Start simple**: Always test in Expo Go before creating custom builds
2. **Follow conventions**: Use Expo Router patterns, kebab-case filenames
3. **Use modern APIs**: Prefer `expo-audio`, `expo-video`, `expo-image`
4. **Handle safe areas**: Use `contentInsetAdjustmentBehavior="automatic"`
5. **Style with CSS**: Use `boxShadow` instead of legacy shadow props
6. **Type everything**: Use TypeScript strict mode, no `any`
7. **Secure tokens**: Use `expo-secure-store` for sensitive data
8. **Environment config**: Use `EXPO_PUBLIC_` prefix for client env vars
---
## 📝 Project Context
**TabataFit** — "Apple Fitness+ for Tabata"
- **Framework**: Expo SDK 52 (managed)
- **Navigation**: Expo Router v3
- **State**: Zustand + AsyncStorage
- **Video**: expo-av → HLS streaming
- **Audio**: expo-av (coaching + music)
- **Animations**: React Native Animated
- **Payments**: RevenueCat
- **Analytics**: PostHog
### Design System
```typescript
BACKGROUND: '#000000' // Pure black
SURFACE: '#1C1C1E' // Charcoal
BRAND: '#FF6B35' // Flame orange
REST: '#5AC8FA' // Ice blue
SUCCESS: '#30D158' // Energy green
```
### Phase Colors (Critical)
```typescript
PREP: '#FF9500' // Orange-yellow
WORK: '#FF6B35' // Flame orange
REST: '#5AC8FA' // Ice blue
COMPLETE: '#30D158' // Green
```
---
*Last updated: March 14, 2026*
# context-mode — MANDATORY routing rules
You have context-mode MCP tools available. These rules are NOT optional — they protect your context window from flooding. A single unrouted command can dump 56 KB into context and waste the entire session.
## BLOCKED commands — do NOT attempt these
### curl / wget — BLOCKED
Any shell command containing `curl` or `wget` will be intercepted and blocked by the context-mode plugin. Do NOT retry.
Instead use:
- `context-mode_ctx_fetch_and_index(url, source)` to fetch and index web pages
- `context-mode_ctx_execute(language: "javascript", code: "const r = await fetch(...)")` to run HTTP calls in sandbox
### Inline HTTP — BLOCKED
Any shell command containing `fetch('http`, `requests.get(`, `requests.post(`, `http.get(`, or `http.request(` will be intercepted and blocked. Do NOT retry with shell.
Instead use:
- `context-mode_ctx_execute(language, code)` to run HTTP calls in sandbox — only stdout enters context
### Direct web fetching — BLOCKED
Do NOT use any direct URL fetching tool. Use the sandbox equivalent.
Instead use:
- `context-mode_ctx_fetch_and_index(url, source)` then `context-mode_ctx_search(queries)` to query the indexed content
## REDIRECTED tools — use sandbox equivalents
### Shell (>20 lines output)
Shell is ONLY for: `git`, `mkdir`, `rm`, `mv`, `cd`, `ls`, `npm install`, `pip install`, and other short-output commands.
For everything else, use:
- `context-mode_ctx_batch_execute(commands, queries)` — run multiple commands + search in ONE call
- `context-mode_ctx_execute(language: "shell", code: "...")` — run in sandbox, only stdout enters context
### File reading (for analysis)
If you are reading a file to **edit** it → reading is correct (edit needs content in context).
If you are reading to **analyze, explore, or summarize** → use `context-mode_ctx_execute_file(path, language, code)` instead. Only your printed summary enters context.
### grep / search (large results)
Search results can flood context. Use `context-mode_ctx_execute(language: "shell", code: "grep ...")` to run searches in sandbox. Only your printed summary enters context.
## Tool selection hierarchy
1. **GATHER**: `context-mode_ctx_batch_execute(commands, queries)` — Primary tool. Runs all commands, auto-indexes output, returns search results. ONE call replaces 30+ individual calls.
2. **FOLLOW-UP**: `context-mode_ctx_search(queries: ["q1", "q2", ...])` — Query indexed content. Pass ALL questions as array in ONE call.
3. **PROCESSING**: `context-mode_ctx_execute(language, code)` | `context-mode_ctx_execute_file(path, language, code)` — Sandbox execution. Only stdout enters context.
4. **WEB**: `context-mode_ctx_fetch_and_index(url, source)` then `context-mode_ctx_search(queries)` — Fetch, chunk, index, query. Raw HTML never enters context.
5. **INDEX**: `context-mode_ctx_index(content, source)` — Store content in FTS5 knowledge base for later search.
## Output constraints
- Keep responses under 500 words.
- Write artifacts (code, configs, PRDs) to FILES — never return them as inline text. Return only: file path + 1-line description.
- When indexing content, use descriptive source labels so others can `search(source: "label")` later.
## ctx commands
| Command | Action |
|---------|--------|
| `ctx stats` | Call the `stats` MCP tool and display the full output verbatim |
| `ctx doctor` | Call the `doctor` MCP tool, run the returned shell command, display as checklist |
| `ctx upgrade` | Call the `upgrade` MCP tool, run the returned shell command, display as checklist |
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **tabatago** (3362 symbols, 9407 relationships, 129 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
## Always Do
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
## Never Do
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
## Resources
| Resource | Use for |
|----------|---------|
| `gitnexus://repo/tabatago/context` | Codebase overview, check index freshness |
| `gitnexus://repo/tabatago/clusters` | All functional areas |
| `gitnexus://repo/tabatago/processes` | All execution flows |
| `gitnexus://repo/tabatago/process/{name}` | Step-by-step execution trace |
## CLI
| Task | Read this skill file |
|------|---------------------|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
<!-- gitnexus:end -->
- **User**: Millian LMX (CEO)
- **Gitea**: https://gitea.1000co.fr/millianlmx/tabatago
- **Supabase**: Managed instance (URL in secrets)
- **App Store**: Not yet published

294
docs/ci-cd-setup.md Normal file
View File

@@ -0,0 +1,294 @@
# TabataGo CI/CD Setup Guide
> How to set up the Mac self-hosted runner and configure secrets for the
> **PR → iPhone → LGTM** workflow.
## Overview
The `pr-iphone-deploy.yml` workflow (`.gitea/workflows/`) automates the
test-and-merge cycle for every PR targeting `main`:
1. **Build** — Checks out the PR, generates the Xcode project via XcodeGen,
and builds the TabataGo app for Millian's iPhone (device build).
2. **Comment** — Posts a "Ready to test" comment on the PR.
3. **Wait for LGTM** — Polls PR comments for up to **2 hours** waiting for
Millian to reply `LGTM` (merge) or `KO` (block).
4. **Auto-merge** — Merges the PR automatically when `LGTM` is received.
```
┌────────────┐ ┌──────────────┐ ┌──────────────┐
│ PR opened │ ──► │ Build iPhone │ ──► │ Wait LGTM/KO │
│ / updated │ │ (macOS CI) │ │ (poll 2h) │
└────────────┘ └──────────────┘ └──┬────────┬──┘
│ │
LGTM │ │ KO
▼ ▼
┌────────┐ ┌────────┐
│ Merge │ │ Block │
│ PR │ │ PR │
└────────┘ └────────┘
```
---
## 1. Set Up the Mac Runner
You need a **macOS machine** (macOS 15+, Xcode 26+) registered as a
self-hosted Gitea Actions runner.
### 1.1 Prerequisites on the Mac
```bash
# Xcode 26+ (from App Store or Apple Developer)
sudo xcode-select -s /Applications/Xcode.app
# XcodeGen (project generator)
brew install xcodegen
# Python 3 (pre-installed on macOS, used by poll script)
python3 --version # should be 3.x
# Xcode must be signed into an Apple Developer account for automatic signing
# Open Xcode → Settings → Accounts → Add your Apple ID
```
### 1.2 Register the Runner in Gitea
1. Go to your Gitea repo: **Settings → Actions → Runners**
2. Click **Create new runner**
3. Follow the instructions to download and configure the runner binary.
Use the label **`macos`** (this must match `runs-on: macos` in the workflow):
```bash
# On the Mac, after downloading the runner:
./act_runner register \
--instance https://gitea.1000co.fr \
--token <RUNNER_TOKEN_FROM_GITEA> \
--name mac-runner \
--labels macos
```
4. Start the runner (as a background service):
```bash
./act_runner daemon
```
For persistence across reboots, create a LaunchAgent:
```xml
<!-- ~/Library/LaunchAgents/com.tabatago.runner.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.tabatago.runner</string>
<key>ProgramArguments</key>
<array>
<string>/path/to/act_runner</string>
<string>daemon</string>
</array>
<key>WorkingDirectory</key>
<string>/path/to/runner</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
```
Then load it:
```bash
launchctl load ~/Library/LaunchAgents/com.tabatago.runner.plist
```
### 1.3 Verify the Runner
Check that the runner appears as **Idle** in:
**Gitea → Settings → Actions → Runners**
---
## 2. Configure Required Secrets
Go to **Gitea → Settings → Actions → Secrets** and add:
### 2.1 `IPHONE_UDID`
The UDID (Unique Device Identifier) of Millian's iPhone.
This tells Xcode which physical device to build for.
```bash
# Find the UDID on the Mac when the iPhone is connected via USB:
xcrun xctrace list devices
# Look for the iPhone line, e.g.:
# Millian's iPhone (26.0) (00008110-XXXXXXXXXXXX)
# Or in Finder: select the iPhone in the sidebar → click the serial number
# line until it switches to UDID
```
| Secret Name | Value Example |
|----------------|------------------------------------|
| `IPHONE_UDID` | `00008110-00123456789ABC01` |
### 2.2 `GITEA_TOKEN`
A Gitea personal access token with **read/write** permissions on the repo.
Create it at: **Gitea → Settings → Applications → Generate Token**
Required permissions:
- `read:repository` — read PR info, comments
- `write:issue` — post comments on PRs
- `write:pull_request` — merge PRs
| Secret Name | Value Example |
|-----------------|----------------------------------------|
| `GITEA_TOKEN` | `e20fd3de2f79b324afde5049e7bafd60a...` |
---
## 3. Code Signing Setup
The workflow uses **automatic code signing** (`CODE_SIGN_STYLE=Automatic`
with `-allowProvisioningUpdates`). This requires:
1. **Apple ID signed into Xcode** on the Mac runner
2. **The iPhone UDID registered** in the Apple Developer account
3. **A valid provisioning profile** for `com.tabatago` (Debug)
To set up:
```bash
# On the Mac runner, open Xcode and sign in:
# Xcode → Settings → Accounts → + → Apple ID
# Verify the team is available:
cd tabatago-swift
xcodegen generate
xcodebuild -showBuildSettings -scheme TabataGo -sdk iphoneos | grep DEVELOPMENT_TEAM
```
If automatic signing doesn't work (first-time setup), you may need to open
the project in Xcode once manually to let it create the provisioning profile.
---
## 4. How the Workflow Works
### Trigger
The workflow runs on:
- `pull_request` events on `main`
- Types: `opened`, `synchronize` (new commits pushed), `reopened`
### Build Job (`build-deploy`)
| Step | What it does |
|------|-------------|
| Checkout | Clones the PR branch |
| Setup Xcode | Selects Xcode.app, prints version |
| Install XcodeGen | `brew install xcodegen` if not present |
| Generate project | `xcodegen generate` in `tabatago-swift/` |
| Build for iPhone | `xcodebuild build` for device UDID from secret |
| Post comment | Posts a "Ready to test" message on the PR |
The build runs with `set -o pipefail` so that any compilation error causes
the job to fail. The last 40 lines of build output are shown; the full log
is saved to `build/xcodebuild.log`.
### Approval Job (`wait-approval`)
- Polls the Gitea API every **30 seconds** for new PR comments
- **Timeout**: 120 minutes (240 iterations × 30s)
- **LGTM detection**: Case-insensitive match for `lgtm` anywhere in the comment body
- **KO detection**: Exact case-insensitive match for a standalone `ko` comment
- On `LGTM` → merges the PR via Gitea API
- On `KO` → fails the workflow (exit code 1)
- On timeout → fails the workflow (exit code 1)
### Comment Format
Reply on the PR with just:
- **`LGTM`** — approve and auto-merge
- **`KO`** — block the PR
---
## 5. Troubleshooting
### Runner doesn't pick up jobs
- Check the runner label: must be `macos` exactly
- Check **Settings → Actions → Runners** — runner must be **Idle**, not **Offline**
- Run `./act_runner daemon` in a terminal to see logs
### Build fails with signing error
```
error: No profiles for 'com.tabatago' were found
```
- Open Xcode on the Mac runner, sign into your Apple ID
- Build once in Xcode to generate the provisioning profile
- Verify the iPhone UDID is correct
### Build fails with "device not found"
```
error: Unable to find a device matching the provided destination specifier
```
- Check that `IPHONE_UDID` secret matches the actual device UDID
- The device must be registered in your Apple Developer account
### Comment not posted / LGTM not detected
- Verify `GITEA_TOKEN` has the required permissions (read/write issues + PRs)
- Check the Gitea instance URL: `https://gitea.1000co.fr`
- Ensure the token hasn't expired
### Workflow runs on every PR push, canceling previous runs
This is intentional — the `concurrency` setting cancels in-progress runs
for the same PR when new commits are pushed. This prevents queue buildup.
---
## 6. File Locations
```
tabatago/
├── .gitea/
│ └── workflows/
│ └── pr-iphone-deploy.yml ← iPhone build + LGTM workflow
├── .github/
│ └── workflows/
│ └── ci.yml ← Admin web + YouTube worker CI
└── docs/
└── ci-cd-setup.md ← This document
```
---
## 7. Required Tools on the Mac Runner
| Tool | Version | Install |
|-----------|----------|----------------------------------|
| macOS | 15+ | — |
| Xcode | 26.0+ | App Store or Apple Developer |
| XcodeGen | latest | `brew install xcodegen` |
| Python 3 | 3.x | Pre-installed on macOS |
| curl | any | Pre-installed on macOS |
| git | any | Pre-installed on macOS |
---
## See Also
- [Gitea Actions Documentation](https://docs.gitea.com/usage/actions/overview)
- [Gitea API — Merge a Pull Request](https://docs.gitea.com/api/1.22/#tag/pull-request/operation/repoMergePullRequest)
- [Xcode Build Settings Reference](https://developer.apple.com/documentation/xcode/build-settings-reference)