Run failed at 'brew install xcodegen node' AFTER both formulae upgraded successfully under ARM: brew's 'installed dependents check' then tried to rebuild gemini-cli (0.40.1 -> 0.46.0, an unrelated dev tool on the runner that depends on node) and exited 1. Verified node is NOT used by any step in pr-iphone-deploy.yml — only xcodegen is. The only node references in the repo are in scripts/prepare-release.cjs and scripts/verify-admin-web-docker.sh, neither of which runs in this workflow. Node was a historical leftover. Drop node from the brew install. No node upgrade = no dependents check = no gemini-cli rebuild. Also sync AGENTS.md §5 step 4.
17 KiB
TabataGo — Référence canonique pour agents IA
Une seule source de vérité. Ce document décrit le projet réel. Si un autre fichier (CLAUDE.md, schema.sql) contredit, ce document gagne.
0. ⚠️ Fichiers obsolètes — NE PAS LIRE
| Fichier | Statut | Pourquoi |
|---|---|---|
CLAUDE.md (racine) |
❌ OBSOLÈTE | Décrit TabataFit, un concept Expo/React Native abandonné |
supabase/schema.sql |
❌ OBSOLÈTE | Header littéral "TabataFit" — décrit l'ancien schéma |
supabase/seed.sql, supabase/setup-admin.sql |
⚠️ Vérifier | Peuvent être périmés ; préférer supabase/migrations/ |
AGENTS.md (ancien, racine) |
🔄 Remplacé par celui-ci | Mentionne PostHog 3.x comme actif (faux, voir §3) |
Source de vérité du schéma : supabase/migrations/001 → 006. Jamais schema.sql.
1. Identité produit
| Nom | TabataGo |
| Bundle | fr.millianlmx.tabatago |
| Team | 2MJF39L8VY |
| But | App d'entraînement Tabata (intervalles 20s travail / 10s repos) sur iOS + watchOS, avec programmes par zone corporelle, HealthKit, musique, et abonnements RevenueCat |
| Version | 1.0 (build 2) — non publiée App Store |
| Propriétaire | Millian LMX (CEO) |
| Gitea | https://gitea.1000co.fr/millianlmx/tabatago |
2. Stack technique
| Couche | Réalité vérifiée |
|---|---|
| Langage | Swift 6.0, concurrence stricte (SWIFT_STRICT_CONCURRENCY: complete) |
| UI | SwiftUI uniquement |
| iOS | 26.0+ |
| watchOS | 11.0+ |
| Build | XcodeGen — project.yml est la source, .xcodeproj est généré |
| SPM | Supabase 2.5.0+, RevenueCat 5.0.0+ |
| ❌ NON installé | PostHog — absent de project.yml packages. Voir §3. |
| Architecture | MVVM + @Observable, AppState.shared central |
| Backend | Supabase (PostgreSQL, Auth, Storage, Edge Functions Deno) |
| Local cache | SwiftData (TabataGoSchema.container) |
| Santé | HealthKit (fréquence cardiaque, calories, workouts) |
| CI/CD | Gitea Actions sur runner macOS auto-hébergé (label macos) |
3. Architecture iOS
Cibles (tabatago-swift/project.yml)
| Cible | Type | Plateforme | Bundle ID |
|---|---|---|---|
TabataGo |
application | iOS 26.0 | fr.millianlmx.tabatago |
TabataGoWatch |
application | watchOS 11.0 | fr.millianlmx.tabatago.watchkitapp |
TabataGoWatchWidget |
app-extension | watchOS 11.0 | fr.millianlmx.tabatago.watchkitapp.widget |
TabataGoTests |
bundle.unit-test | iOS | .tests |
TabataGoUITests |
bundle.ui-testing | iOS | .uitests |
Dépendances d'intégration : TabataGo embed TabataGoWatch qui embed TabataGoWatchWidget.
Fichier partagé entre iOS et watch : TabataGo/Services/WatchConnectivityTypes.swift (compilé dans les deux targets via group: TabataGoWatch/Services).
Bootstrap & data flow
TabataGoApp (@main)
└─ RootView()
├─ .environment(AppState.shared) // @Observable singleton
├─ .modelContainer(TabataGoSchema.container) // SwiftData
└─ .task { await AppState.shared.bootstrap() }
AppState.bootstrap() (MainActor, idempotent, skip en preview) :
PurchaseService.shared.initialize()(RevenueCat)AnalyticsService.shared.initialize()— ⚠️ no-op par défaut car PostHog n'est pas dans SPM : tout est gardé par#if canImport(PostHog)qui est toujours faux. La taxonomie d'events existe mais n'émet rien tant que la dépendance n'est pas ajoutée.
Navigation (PAS de NavigationStack)
MainTabView = TabView + Tab(value:) (Liquid Glass, iOS 26), 4 onglets dans cet ordre exact du code :
home → programs → activity → profile
Modals full-screen via .sheet/fullScreenCover au-dessus :
PlayerView, CompletionView, PaywallView, OnboardingView.
Services (TabataGo/Services/)
| Service | Rôle |
|---|---|
SupabaseService |
Client Supabase (Auth, DB, Storage) |
HealthKitService |
Lecture/écriture HealthKit |
PurchaseService |
RevenueCat IAP, état d'abonnement |
MusicService |
Apple Music + intégration piste YouTube |
AudioService |
Playback audio (coachs sonores, alerts) |
AnalyticsService |
Stub PostHog (voir ci-dessus) |
PhoneConnectivityManager |
Côté iOS — WatchConnectivity (échange avec la montre) |
WatchConnectivityTypes |
Protocoles/messages partagés iOS↔Watch (compilé dans les 2 targets) |
Le miroir côté watch est
TabataGoWatch/Services/WatchConnectivityManager.swift(cible watchOS, pas iOS).
ViewModels (@Observable)
HomeViewModel, HealthViewModel, PlayerViewModel, PurchaseViewModel, MusicPlayerViewModel.
Models (TabataGo/Models/)
WorkoutProgram, WorkoutSession, UserProfile, HealthSnapshot, MusicTrack, WorkoutActivityAttributes, MusicActivityAttributes (Live Activities), TabataGoSchema (SwiftData), PreviewData/MockPrograms (previews).
Watch (TabataGoWatch/)
TabataGoWatchApp → WatchRootView → états WatchIdleView / WatchActivityView / WatchPlayerView.
Moteur : WatchPlayerEngine. Connectivité : WatchConnectivityManager (coté watch).
Complications : TabataGoComplication.
4. Base de données
Supabase — source = supabase/migrations/ (001→006)
| # | Fichier | Contenu |
|---|---|---|
| 001 | 001_initial_schema.sql |
trainers, workouts, collections, achievements, admin_users |
| 002 | 002_download_jobs.sql |
download_jobs, download_items (jobs d'import playlist YouTube) |
| 003 | 003_music_genre.sql |
music_genre |
| 004 | 004_download_items_public_read.sql |
RLS : lecture publique de download_items |
| 005 | 005_workout_programs.sql |
DROP programs/program_workouts → CREATE workout_programs (body_zone enum upper-body/lower-body/full-body + level Beginner/Intermediate/Advanced) et program_tabatas (3 tabatas/program, 2 exercices chacun, 8 rounds × 20s/10s) + RLS public read + admin all |
| 006 | 006_seed_workout_programs.sql |
18 programmes seedés (12 free : 4 par zone en 2B+1I+1A ; 6 premium) + 54 tabatas (18×3), UUIDs stables |
Tables actives post-migration : trainers, workouts, collections, achievements, admin_users, download_jobs, download_items, music_genre, workout_programs, program_tabatas.
Tables supprimées (ne pas recréer) : programs, program_workouts (DROP en 005).
RLS : lecture publique sur workout_programs/program_tabatas/download_items ; écriture restreinte aux admin_users (EXISTS (SELECT 1 FROM admin_users WHERE id = auth.uid())).
SwiftData — cache local
TabataGoSchema.container (et .previewContainer pour les #Preview). Persistance offline des sessions/programmes côté app. Ne pas confondre avec Supabase.
5. CI/CD
Workflow : .github/workflows/pr-iphone-deploy.yml
Trigger : PR ouverte/synchronize/reopened sur main.
Runner : label macos (auto-hébergé). timeout-minutes: 30 (build-deploy), 120 (wait-approval).
Concurrency : group: pr-iphone-deploy-<PR#>, cancel-in-progress: true → un push sur une PR existante annule le pipeline précédent (pas de double-merge, pas de runner gaspillé). Différentes PRs tournent en parallèle.
Déroulé :
- Checkout (shallow, branche head) via
PR_API_TOKEN. - Setup PATH :
echo "/opt/homebrew/bin" >> $GITHUB_PATH— requis car Rosetta ne le voit pas. - Clean ciblé :
- Supprime
build/derived,TabataGo.xcodeproj,Package.resolved, DerivedData, ModuleCache. - GARDE le cache SPM (
build/spm-cache— c'est le chemin passé à-clonedSourcePackagesDirPath) — RevenueCat ≈ 1.1 GiB, re-cloner = 5+ min et échecs aléatoires du sandbox. Ne jamais fairerm -rf build/: ça détruirait ce cache. Le clean step ne touche quebuild/derived. - Ne touche pas à
~/Library/Caches/com.apple.dt.Xcode(cache Xcode global — le nuke ralentit tous les builds suivants).
- Supprime
- Install tools :
arch -arm64 brew install xcodegen(sans|| true— si brew down, on fail ici, pas deux steps plus loin).arch -arm64requis car le shell act_runner tourne sous Rosetta 2 sur Apple Silicon, mais Homebrew est dans le prefix ARM/opt/homebrew— sans ça, brew refuse d'installer/upgrader. Seul xcodegen est nécessaire : node n'est pas utilisé par ce workflow, et l'installer déclenche le "installed dependents check" de brew qui essaie de rebuilder des formulae sans rapport (ex.gemini-cli) et fail le build. - Write
Config/Secrets.xcconfigdepuis les secrets Gitea (SUPABASE_URL,SUPABASE_ANON_KEY,REVENUECAT_API_KEY,POSTHOG_API_KEY). Preflight : fail loud si un secret requis manque. Le fichier est gitigné (règle #9) ; CI l'écrase à chaque run.Info.plistlit les valeurs via$(SUPABASE_URL)etc., etproject.ymlwire le xcconfig en Debug+Release — aucun autre changement nécessaire côté build. - xcodegen generate →
xcodebuild -resolvePackageDependencies→ build (-scheme TabataGo, Debug, auto-provisioning, team2MJF39L8VY). Le scheme build les 4 targets :TabataGo(app iOS),TabataGoWidget(widget iOS, vrai target — ne pas confondre avecTabataGoWatchWidgetqui est watchOS),TabataGoWatch(app watchOS),TabataGoWatchWidget(complication watchOS). - Deploy iPhone UDID
00008120-000925CE3672201E:xcrun devicectl device install appuniquement.devicectlgère nativement la découverte WiFi (réseau) et filaire (USB-C/Thunderbolt) — pas de fallbackios-deploy. - Post comment "Prêt à tester" sur la PR.
- Job
wait-approval: poll (30s, max 240 = 2h). Re-fetche TOUS les comments chaque cycle (passince_id) pour attraper aussi les edits (un reviewer passant de "KO" à "LGTM"). Match LGTM/KO n'importe où dans le body (regex\bLGTM\b/\bKO\b, case-insensitive) — "Tested, LGTM!" compte ; "KOM" ne compte pas.LGTM→ squash-merge ({"Do":"squash"}).KO→ blocage. Timeout → fail.
Secrets
| Secret | Usage |
|---|---|
PR_API_TOKEN |
Checkout + API Gitea (comment, merge). JAMAIS GITEA_TOKEN/GITHUB_TOKEN. Tout passe par l'API Gitea avec ce token — un bloc permissions: natif serait no-op, c'est pourquoi il n'y en a pas. |
SUPABASE_URL, SUPABASE_ANON_KEY, REVENUECAT_API_KEY, POSTHOG_API_KEY |
Un step CI écrit Config/Secrets.xcconfig depuis ces 4 secrets avant xcodegen generate. Info.plist lit les valeurs via $(SUPABASE_URL) etc. ; project.yml wire le xcconfig en Debug+Release. POSTHOG_API_KEY peut être vide (PostHog pas encore câblé en SPM). |
⚠️ Historique :
Config/Secrets.xcconfiga été committé par erreur dans le passé (avant la règle gitignore) avec des valeurs réelles. Le fichier est maintenant gitigné et écrasé par CI, mais l'historique git contient encore l'ancienne version — rotation des clés Supabase/RevenueCat recommandée côté ops.
⚠️ Pitfalls CI
/opt/homebrew/binhors PATH par défaut sous Rosetta → toujours l'ajouter.- xcodegen 2.45.4 ne génère pas de schemes auto → scheme
TabataGodéfini explicitement dansproject.yml(ne pas le supprimer). Il liste 4 targets :TabataGo,TabataGoWidget,TabataGoWatch,TabataGoWatchWidget. - Ne jamais
rm -rf build/nirm -rf build/spm-cachedans le CI — RevenueCat y vit. Le clean step ne touche quebuild/derived. SWIFT_ENABLE_EXPLICIT_MODULES=NOau build — requis sinon segfault linker sur.pcmstale.-skipPackagePluginValidation -allowProvisioningUpdates.- Concurrency : le bloc
concurrency:est volontaire — ne pas le retirer, sinon les pushes successifs empilent des pipelines et tentent des double-merges. - LGTM/KO regex : matche n'importe où dans le body avec
\b...\b(word boundary). Ne pas revenir à ungrep '"body": *"LGTM"'ancré — il raterait "Tested, LGTM!". - Squash-merge (
{"Do":"squash"}) — pas merge commit ni rebase. Garde l'historiquemainlinéaire. - Comment edits :
wait-approvalre-fetche tous les comments chaque cycle (passince_id) pour attraper les edits.
6. Sous-projets
admin-web/ — Dashboard admin
Next.js 15 App Router, shadcn/ui. Gestion workouts / trainers / collections / programs.
Auth Supabase → table admin_users. Stack : TypeScript, middleware.ts, tests Playwright (e2e/) + Vitest.
youtube-worker/ — Worker YouTube
Node.js (server.js, package.json, Dockerfile). Télécharge l'audio de playlists YouTube → Supabase Storage. Piloté par les Edge Functions ci-dessous.
supabase/functions/ — Edge Functions (Deno)
| Function | Rôle |
|---|---|
youtube-playlist/ |
Crée un download_job, liste les vidéos |
youtube-process/ |
Orchestre le téléchargement (via youtube-worker) |
youtube-status/ |
Statut d'un job |
youtube-classify/ |
Classification genre musical |
main/ |
Auth JWT (helper jose) |
_shared/ |
auth.ts, cors.ts, supabase-client.ts, youtube-client.ts |
Le pipeline musical complet : Edge Functions orchestrent →
youtube-worker(Node.js) télécharge via yt-dlp + Innertube → audio dans Supabase Storage (workout-audiobucket) → classification automatique par Gemini (gemini-3.1-flash-lite-preview) → métadonnées dansdownload_jobs/download_items→ genre dansmusic_genre.
7. Règles d'or pour les agents
- Source de vérité =
project.yml+supabase/migrations/. Tout le reste est dérivé ou obsolète. - SwiftUI only. Pas d'UIKit sauf nécessité absolue démontrée.
@Observable(macro Observation), jamais@ObservableObject/@Published.- Concurrency stricte.
async/awaitpartout,@MainActorsur l'UI, pas de completion handlers. - Pas de force-unwrap.
guard let/if let, jamais!. - Navigation = TabView + sheet/fullScreenCover. Pas de
NavigationStack. - Tout en français côté user-facing (
Localizable.xcstrings/L10n). Codes/types restent en anglais. - XcodeGen. Éditer
project.yml, pas.xcodeproj. Régénérer avecxcodegen generate. - Secrets. Jamais committer
Config/Secrets.xcconfig. Template =.example. En CI, un step écrit ce fichier depuis les secrets Gitea (SUPABASE_URL,SUPABASE_ANON_KEY,REVENUECAT_API_KEY,POSTHOG_API_KEY) avantxcodegen generate;Info.plistlit les valeurs via$(VAR)etproject.ymlwire le xcconfig en Debug+Release. - Lazy d'abord. Avant d'ajouter une dépendance : stdlib Apple, puis ce qui existe déjà dans le repo, puis SPM déjà installé. PostHog n'est pas installé — ne pas l'ajouter sans justification.
8. Anti-patterns / pièges documentés
| ❌ Ne pas faire | ✅ Faire |
|---|---|
Lire CLAUDE.md ou schema.sql pour le schéma |
Lire supabase/migrations/001→006 |
| Importer PostHog comme si c'était actif | Savoir que AnalyticsService est un no-op (PostHog pas en SPM) |
Utiliser NavigationStack |
TabView + modals |
@ObservableObject / @Published |
@Observable |
Force-unwrap ! |
guard let |
| Strings UI en anglais | Françaises dans L10n/Localizable.xcstrings |
rm -rf build/ ou rm -rf build/spm-cache en CI |
Garder build/spm-cache ; ne cleaner que build/derived |
rm -rf ~/Library/Caches/com.apple.dt.Xcode |
Laisser le cache Xcode global intact (sinon tous les builds ralentissent) |
Utiliser GITEA_TOKEN en CI |
PR_API_TOKEN uniquement |
Compter sur un bloc permissions: natif Gitea Actions |
Tout passe par l'API Gitea avec PR_API_TOKEN ; le bloc natif serait no-op |
Retirer le bloc concurrency: |
Permet double-merge et pipelines empilés |
Ancre le grep LGTM au début du body ('"body": *"LGTM"') |
Regex \bLGTM\b n'importe où dans le body (attrape "Tested, LGTM!") |
{"Do":"merge"} (merge commit) |
{"Do":"squash"} pour un main linéaire |
Supprimer le scheme explicite dans project.yml |
xcodegen 2.45.4 n'en crée pas — scheme doit lister TabataGo/TabataGoWidget/TabataGoWatch/TabataGoWatchWidget |
Compter sur admin-web/ pour la app iOS |
Dashboard admin séparé, communique via Supabase uniquement |
Recréer programs/program_workouts |
Remplacés par workout_programs/program_tabatas (migration 005) |
9. Contexte projet & skills
- Repo Gitea :
https://gitea.1000co.fr/millianlmx/tabatago(branche intégration :main). - Équipe : "Millian Team" — équipe d'agents IA. Skills à loader selon la tâche :
senior-ios— SwiftUI, Xcode, iOS/watchOSsenior-backend— Supabase, SQL, Edge Functionssenior-devops— CI/CD Gitea Actions, deploypo-pm— vision produit, specs, validation
- Team skill TabataGo : documente les anti-patterns ci-dessus ; aligne tous les agents sur les mêmes conventions.
- Hermes profile actif :
default. Les skills vivent dans le profile sousskills/. - Docs utiles :
docs/ci-cd-setup.md,docs/app-store-submission.md,docs/maestro-e2e-testing-strategy.md,docs/ui-feature-brief.md. - Scripts :
scripts/ci-status.py,scripts/deploy-functions.sh.
Commandes usuelles
# Générer le projet Xcode
cd tabatago-swift && xcodegen generate
# Ouvrir
open tabatago-swift/TabataGo.xcodeproj
# Déployer les Edge Functions
bash scripts/deploy-functions.sh
# Statut CI
python3 scripts/ci-status.py