Compare commits
6 Commits
ci/iphone-
...
f371d8428d
| Author | SHA1 | Date | |
|---|---|---|---|
| f371d8428d | |||
| 36658fe7d1 | |||
| c1cbc02826 | |||
| 87e0610a62 | |||
| 1ca8c3a467 | |||
| 8116b18a38 |
@@ -1,192 +1,239 @@
|
||||
# =============================================================================
|
||||
# TabataGo — PR → Build → USB Direct Deploy → Wait LGTM → Merge
|
||||
# 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 → Build → USB Deploy → LGTM
|
||||
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 & Deploy to iPhone via USB
|
||||
name: Build & Install on iPhone
|
||||
runs-on: macos
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
git clone --depth 1 -b ${{ github.head_ref }} https://x-access-token:${PR_TOKEN}@gitea.1000co.fr/${{ github.repository }}.git .
|
||||
env:
|
||||
PR_TOKEN: ${{ secrets.PR_API_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Xcode
|
||||
run: |
|
||||
xcodebuild -version
|
||||
sudo xcode-select -s /Applications/Xcode.app
|
||||
echo "Using Xcode $(xcodebuild -version | head -1)"
|
||||
|
||||
- name: Setup PATH
|
||||
run: echo "/opt/homebrew/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Clean build artifacts
|
||||
- name: Install XcodeGen (if needed)
|
||||
run: |
|
||||
echo "🧹 Cleaning build artifacts..."
|
||||
|
||||
# Act runner caches
|
||||
rm -rf ~/.cache/act/*/hostexecutor/tabatago-swift/build 2>/dev/null || true
|
||||
|
||||
# SPM caches are KEPT between runs — RevenueCat is 1.10 GiB and takes
|
||||
# 5+ minutes to clone; re-cloning on every CI run would waste time
|
||||
# and cause random failures on the act_runner sandbox.
|
||||
|
||||
# Xcode DerivedData — suppression COMPLÈTE (le bundle ID a changé,
|
||||
# l'ancien dossier TabataGo-* peut être ignoré par le nouveau build)
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
||||
|
||||
# Module cache Xcode — des .pcm stale peuvent segfault le linker
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex 2>/dev/null || true
|
||||
rm -rf ~/Library/Caches/com.apple.dt.Xcode
|
||||
|
||||
# Build artifacts locaux (utilisés par le step "Build app")
|
||||
rm -rf build/
|
||||
rm -rf .build
|
||||
rm -rf tabatago-swift/.build
|
||||
rm -rf tabatago-swift/TabataGo.xcodeproj
|
||||
rm -rf tabatago-swift/*.xcworkspace 2>/dev/null || true
|
||||
|
||||
# Package.resolved — peut être stale après un changement de bundle ID
|
||||
find . -name "Package.resolved" -delete 2>/dev/null || true
|
||||
find . -name ".package.resolved" -delete 2>/dev/null || true
|
||||
|
||||
echo "✅ Caches nettoyés"
|
||||
|
||||
- name: Install tools (xcodegen, node, ios-deploy)
|
||||
run: |
|
||||
brew install xcodegen node ios-deploy || true
|
||||
if ! command -v xcodegen &> /dev/null; then
|
||||
brew install xcodegen
|
||||
fi
|
||||
|
||||
- name: Generate Xcode project
|
||||
run: |
|
||||
cd tabatago-swift
|
||||
xcodegen generate
|
||||
|
||||
- name: Resolve SPM packages
|
||||
- name: Build for iPhone
|
||||
env:
|
||||
IPHONE_UDID: ${{ secrets.IPHONE_UDID }}
|
||||
run: |
|
||||
cd tabatago-swift
|
||||
xcodebuild -resolvePackageDependencies \
|
||||
-project TabataGo.xcodeproj \
|
||||
-scmProvider system
|
||||
|
||||
- name: Build app
|
||||
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 \
|
||||
-project TabataGo.xcodeproj \
|
||||
-scheme TabataGo \
|
||||
-sdk iphoneos \
|
||||
-destination "platform=iOS,id=${IPHONE_UDID}" \
|
||||
-configuration Debug \
|
||||
-allowProvisioningUpdates \
|
||||
-skipPackagePluginValidation \
|
||||
-derivedDataPath ../build/derived \
|
||||
-clonedSourcePackagesDirPath ../build/spm-cache \
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES=NO \
|
||||
ONLY_ACTIVE_ARCH=NO \
|
||||
-derivedDataPath ../build \
|
||||
-scmProvider system \
|
||||
CODE_SIGN_STYLE=Automatic \
|
||||
DEVELOPMENT_TEAM=2MJF39L8VY
|
||||
2>&1 | tee ../build/xcodebuild.log | tail -40
|
||||
|
||||
- name: Install to iPhone (WiFi → USB fallback)
|
||||
run: |
|
||||
UDID="00008120-000925CE3672201E"
|
||||
APP=$(find ../build/derived -name 'TabataGo.app' -type d | head -1)
|
||||
|
||||
# 1. Try WiFi first (Xcode 15+ devicectl network discovery)
|
||||
echo "📱 Trying WiFi install via devicectl..."
|
||||
xcrun devicectl device install app --device "$UDID" "$APP" 2>&1 | tail -3
|
||||
if [ ${PIPESTATUS[0]} -eq 0 ]; then
|
||||
echo "✅ Installed via WiFi"
|
||||
exit 0
|
||||
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
|
||||
|
||||
# 2. Fallback to USB via ios-deploy
|
||||
echo "🔌 WiFi failed, trying USB..."
|
||||
ios-deploy --bundle "$APP" --id "$UDID" --justlaunch
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Installed via USB"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "❌ Install failed (both WiFi and USB)"
|
||||
exit 1
|
||||
echo ""
|
||||
echo "✅ App built successfully for iPhone (UDID: ${IPHONE_UDID})"
|
||||
|
||||
- name: Post "Ready to test" comment
|
||||
if: success()
|
||||
env:
|
||||
GT_TOKEN: ${{ secrets.PR_API_TOKEN }}
|
||||
GITEA_URL: https://gitea.1000co.fr
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_HOST: https://gitea.1000co.fr
|
||||
run: |
|
||||
PR="${{ github.event.pull_request.number }}"
|
||||
REPO="${{ github.repository }}"
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GT_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"body\":\"## 📱 Prêt à tester !\\n\\nL'app est déployée sur l'iPhone (USB).\\n\\n- Teste les changements\\n- Reply **LGTM** pour merger\\n- Reply **KO** pour bloquer\"}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/issues/${PR}/comments"
|
||||
|
||||
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
|
||||
timeout-minutes: 120 # 2 heures max
|
||||
|
||||
steps:
|
||||
- name: Poll for LGTM
|
||||
env:
|
||||
GT_TOKEN: ${{ secrets.PR_API_TOKEN }}
|
||||
GITEA_URL: https://gitea.1000co.fr
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
GITEA_HOST: https://gitea.1000co.fr
|
||||
run: |
|
||||
PR="${{ github.event.pull_request.number }}"
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
REPO="${{ github.repository }}"
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
API="${GITEA_URL}/api/v1/repos/${REPO}"
|
||||
MAX=240
|
||||
API="${GITEA_HOST}/api/v1/repos/${REPO}"
|
||||
MAX_TRIES=240 # 2h = 240 × 30s
|
||||
TRIES=0
|
||||
|
||||
echo "⏳ Attente LGTM sur PR #${PR}..."
|
||||
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 ""
|
||||
|
||||
LAST_ID=$(curl -s -H "Authorization: token ${GT_TOKEN}" \
|
||||
"${API}/issues/${PR}/comments" | \
|
||||
python3 -c "import json,sys; cs=json.load(sys.stdin); print(cs[-1]['id'] if cs else 0)" 2>/dev/null || echo 0)
|
||||
# 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)
|
||||
|
||||
while [ $TRIES -lt $MAX ]; do
|
||||
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))
|
||||
|
||||
COMMENTS=$(curl -s -H "Authorization: token ${GT_TOKEN}" \
|
||||
"${API}/issues/${PR}/comments?since_id=${LAST_ID}")
|
||||
# Récupérer tous les commentaires
|
||||
COMMENTS=$(curl -sk \
|
||||
-H "Authorization: token $GITEA_TOKEN \
|
||||
"${API}/issues/${PR_NUMBER}/comments" 2>/dev/null)
|
||||
|
||||
if echo "$COMMENTS" | grep -qi '"body": *"LGTM"'; then
|
||||
echo "✅ LGTM reçu ! Merge..."
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GT_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"Do\":\"merge\"}" \
|
||||
"${API}/pulls/${PR}/merge"
|
||||
echo "✅ Mergée."
|
||||
exit 0
|
||||
if [ -z "$COMMENTS" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if echo "$COMMENTS" | grep -qi '"body": *"KO"'; then
|
||||
echo "❌ KO reçu. Bloquée."
|
||||
# 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 " ⏳ ... (${TRIES}/240, $(date +%H:%M))"
|
||||
echo " ⏳ Toujours en attente... (${TRIES}/240, $(date +%H:%M:%S))"
|
||||
fi
|
||||
done
|
||||
echo "❌ Timeout 2h."
|
||||
|
||||
echo ""
|
||||
echo "❌ Timeout — pas de LGTM reçu après 2 heures."
|
||||
exit 1
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -58,11 +58,3 @@ Config/Secrets.xcconfig
|
||||
_Users_*
|
||||
swift-generated-sources/
|
||||
tabatago-swift/build/
|
||||
|
||||
# Swift Package Manager
|
||||
Package.resolved
|
||||
.build/
|
||||
DerivedData/
|
||||
|
||||
# XcodeGen generated — NOT in version control
|
||||
TabataGo.xcodeproj
|
||||
|
||||
@@ -44,7 +44,7 @@ Or run manually:
|
||||
2. Archives the app with Xcode auto-signing
|
||||
3. Exports an App Store IPA
|
||||
4. Uploads to App Store Connect
|
||||
5. The first upload **automatically creates** the app record in App Store Connect (bundle ID: `fr.millianlmx.tabatago`)
|
||||
5. The first upload **automatically creates** the app record in App Store Connect (bundle ID: `com.tabatago.app`)
|
||||
|
||||
## After Upload
|
||||
|
||||
@@ -60,7 +60,7 @@ Or run manually:
|
||||
| Symptom | Likely Cause |
|
||||
|---|---|
|
||||
| "No accounts with iTunes Connect access" | API key doesn't have App Manager permissions — recreate the key with correct access |
|
||||
| "No profiles found" | The bundle ID `fr.millianlmx.tabatago` isn't registered yet — check Apple Developer portal |
|
||||
| "No profiles found" | The bundle ID `com.tabatago.app` isn't registered yet — check Apple Developer portal |
|
||||
| "Duplicate build number" | Build number already used — bump `CFBundleVersion` in `project.yml` and re-tag |
|
||||
| "Authentication failed" | API key was revoked, expired, or secret is misspelled — verify secrets in repository settings |
|
||||
|
||||
|
||||
@@ -1,200 +1,294 @@
|
||||
# CI/CD Setup — iPhone Deploy Workflow
|
||||
# TabataGo CI/CD Setup Guide
|
||||
|
||||
This guide covers setting up a **Gitea Actions** macOS runner to automatically build, test, and deploy **TabataGo** to a physical iPhone on every PR to `main`.
|
||||
> How to set up the Mac self-hosted runner and configure secrets for the
|
||||
> **PR → iPhone → LGTM** workflow.
|
||||
|
||||
## Prerequisites
|
||||
## Overview
|
||||
|
||||
### Hardware
|
||||
- **Mac** running macOS 15 (Sequoia) or later
|
||||
- **Xcode 26+** installed (required for iOS 26.0 / watchOS 11.0 targets)
|
||||
- **iPhone** (iOS 26+) with the Firebase App Tester app installed
|
||||
The `pr-iphone-deploy.yml` workflow (`.gitea/workflows/`) automates the
|
||||
test-and-merge cycle for every PR targeting `main`:
|
||||
|
||||
### Software
|
||||
- [Gitea Actions Runner](https://docs.gitea.com/usage/actions/act-runner) installed and registered
|
||||
- `brew` (Homebrew) installed
|
||||
- `xcodegen` — `brew install xcodegen` (also installed automatically by the workflow)
|
||||
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.
|
||||
|
||||
## 1. Register the macOS Runner
|
||||
|
||||
```bash
|
||||
# On the Mac:
|
||||
./act_runner register \
|
||||
--instance https://gitea.1000co.fr \
|
||||
--token <RUNNER_TOKEN> \
|
||||
--name "macos-runner" \
|
||||
--labels macos
|
||||
|
||||
# Start the runner
|
||||
./act_runner daemon
|
||||
```
|
||||
┌────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ PR opened │ ──► │ Build iPhone │ ──► │ Wait LGTM/KO │
|
||||
│ / updated │ │ (macOS CI) │ │ (poll 2h) │
|
||||
└────────────┘ └──────────────┘ └──┬────────┬──┘
|
||||
│ │
|
||||
LGTM │ │ KO
|
||||
▼ ▼
|
||||
┌────────┐ ┌────────┐
|
||||
│ Merge │ │ Block │
|
||||
│ PR │ │ PR │
|
||||
└────────┘ └────────┘
|
||||
```
|
||||
|
||||
> **Important:** The runner must have the label `macos` because the workflow uses `runs-on: macos`.
|
||||
---
|
||||
|
||||
## 2. Configure Secrets
|
||||
## 1. Set Up the Mac Runner
|
||||
|
||||
Add these secrets in the Gitea repository:
|
||||
`Settings → Actions → Secrets`
|
||||
You need a **macOS machine** (macOS 15+, Xcode 26+) registered as a
|
||||
self-hosted Gitea Actions runner.
|
||||
|
||||
| Secret Name | Description | How to Get |
|
||||
|-------------|-------------|------------|
|
||||
| `FIREBASE_APP_ID` | Firebase App ID (iOS app) | Firebase Console → Project Settings → General → App ID |
|
||||
| `FIREBASE_SERVICE_ACCOUNT` | Base64-encoded Firebase service account JSON key | See [Firebase Setup](#6-firebase-app-distribution-setup) below |
|
||||
| `GITEA_TOKEN` | Gitea API token with repo + pull request scope | `Gitea → Settings → Applications → Generate Token` |
|
||||
### 1.1 Prerequisites on the Mac
|
||||
|
||||
### Gitea Token Permissions
|
||||
The token needs:
|
||||
- `read:repository` — to read PR metadata
|
||||
- `write:issue` — to post LGTM comments
|
||||
- `write:pull_request` — to merge approved PRs
|
||||
```bash
|
||||
# Xcode 26+ (from App Store or Apple Developer)
|
||||
sudo xcode-select -s /Applications/Xcode.app
|
||||
|
||||
## 3. Xcode Signing
|
||||
# XcodeGen (project generator)
|
||||
brew install xcodegen
|
||||
|
||||
The workflow uses **automatic code signing**. Ensure:
|
||||
# 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):
|
||||
|
||||
1. The Mac runner is signed into an Apple Developer account in Xcode:
|
||||
```bash
|
||||
# Verify:
|
||||
xcrun security find-identity -v -p codesigning
|
||||
# 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
|
||||
```
|
||||
|
||||
2. The Apple ID used must be part of the team `2MJF39L8VY` (configured in `ExportOptions.plist`).
|
||||
4. Start the runner (as a background service):
|
||||
|
||||
3. For first-time setup, open Xcode on the runner Mac and sign in:
|
||||
```
|
||||
Xcode → Settings → Accounts → Add Apple ID
|
||||
```bash
|
||||
./act_runner daemon
|
||||
```
|
||||
|
||||
## 4. Workflow Overview
|
||||
For persistence across reboots, create a LaunchAgent:
|
||||
|
||||
The workflow at `.gitea/workflows/pr-iphone-deploy.yml` triggers on PRs to `main`:
|
||||
```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>
|
||||
```
|
||||
|
||||
```
|
||||
PR Opened / Updated
|
||||
↓
|
||||
Checkout + XcodeGen generate
|
||||
↓
|
||||
Resolve SPM packages
|
||||
↓
|
||||
Build + Archive IPA
|
||||
↓
|
||||
Upload to Firebase App Distribution
|
||||
↓
|
||||
Post "Ready to test" comment on PR
|
||||
↓
|
||||
Wait for LGTM → Auto-merge
|
||||
```
|
||||
Then load it:
|
||||
```bash
|
||||
launchctl load ~/Library/LaunchAgents/com.tabatago.runner.plist
|
||||
```
|
||||
|
||||
## 5. Manual Test Run
|
||||
### 1.3 Verify the Runner
|
||||
|
||||
To test the workflow locally on the runner Mac:
|
||||
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
|
||||
# Simulate the build:
|
||||
# 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 build \
|
||||
-project TabataGo.xcodeproj \
|
||||
-target TabataGo \
|
||||
-sdk iphoneos \
|
||||
-destination "generic/platform=iOS" \
|
||||
-configuration Release \
|
||||
-allowProvisioningUpdates \
|
||||
-skipPackagePluginValidation \
|
||||
CODE_SIGN_STYLE=Automatic
|
||||
xcodebuild -showBuildSettings -scheme TabataGo -sdk iphoneos | grep DEVELOPMENT_TEAM
|
||||
```
|
||||
|
||||
## 6. Firebase App Distribution Setup
|
||||
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.
|
||||
|
||||
### 6.1 Create a Firebase Project
|
||||
---
|
||||
|
||||
1. Go to [Firebase Console](https://console.firebase.google.com)
|
||||
2. Click **Add project** (or select an existing one)
|
||||
3. Follow the wizard to create the project (Google Analytics is optional)
|
||||
## 4. How the Workflow Works
|
||||
|
||||
### 6.2 Enable App Distribution
|
||||
### Trigger
|
||||
|
||||
1. In the Firebase Console, open your project
|
||||
2. Navigate to **App Distribution** in the left sidebar
|
||||
3. Click **Get started** to enable the service
|
||||
The workflow runs on:
|
||||
- `pull_request` events on `main`
|
||||
- Types: `opened`, `synchronize` (new commits pushed), `reopened`
|
||||
|
||||
### 6.3 Get the App ID
|
||||
### Build Job (`build-deploy`)
|
||||
|
||||
1. Go to **Project Settings** (gear icon) → **General**
|
||||
2. Scroll down to **Your apps** → find the iOS app
|
||||
3. Copy the **App ID** (looks like `1:123456789012:ios:abc123def456`)
|
||||
| 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 |
|
||||
|
||||
### 6.4 Create a Service Account
|
||||
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`.
|
||||
|
||||
1. Go to **Project Settings** → **Service accounts**
|
||||
2. Click **Create service account** (or use Firebase Admin SDK → Generate new private key)
|
||||
3. Give it a name like `github-actions-deploy`
|
||||
4. Assign the role: **Firebase App Distribution Admin**
|
||||
5. Click **Generate new private key** → download the JSON file
|
||||
### Approval Job (`wait-approval`)
|
||||
|
||||
### 6.5 Encode the Service Account Key
|
||||
- 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)
|
||||
|
||||
```bash
|
||||
# Convert the JSON key to a base64 string (single line, no wrapping)
|
||||
base64 -i firebase-key.json | tr -d '\n'
|
||||
### 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
|
||||
```
|
||||
|
||||
Copy the output — this is your `FIREBASE_SERVICE_ACCOUNT` secret value.
|
||||
- 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
|
||||
|
||||
### 6.6 Add Testers and Groups
|
||||
### Build fails with "device not found"
|
||||
|
||||
1. In Firebase Console, go to **App Distribution** → **Testers & Groups**
|
||||
2. Click **Add group** → name it `millian-test`
|
||||
3. Click **Add testers** → enter Millian's email
|
||||
4. Add the tester to the `millian-test` group
|
||||
|
||||
### 6.7 Install the Firebase App Tester on the iPhone
|
||||
|
||||
1. Send an invite to Millian's iPhone from the Firebase Console
|
||||
2. Millian accepts the invite and installs the **Firebase App Tester** app from the App Store
|
||||
3. Open the app and sign in with the invited Google account
|
||||
4. The app will now receive builds pushed from the CI workflow
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| "Code signing failed" | Sign into Xcode on the runner Mac, verify team membership |
|
||||
| "No provisioning profile" | Run `-allowProvisioningUpdates` or open project in Xcode once |
|
||||
| `xcodegen: command not found` | `brew install xcodegen` (workflow handles this) |
|
||||
| Runner doesn't pick up job | Verify runner label is `macos` and runner is connected |
|
||||
| Token permission errors | Ensure `GITEA_TOKEN` has `write:issue` and `write:pull_request` scopes |
|
||||
| Firebase upload fails | Verify `FIREBASE_APP_ID` and `FIREBASE_SERVICE_ACCOUNT` secrets are set correctly |
|
||||
| "App not found" in Firebase | Check that the iOS app is registered in Firebase with the correct bundle ID |
|
||||
| Testers don't receive build | Verify tester is in the `millian-test` group and has accepted the invite |
|
||||
|
||||
## Compilation Error Troubleshooting
|
||||
|
||||
### xcodegen Clearing Signing Team
|
||||
|
||||
After running `xcodegen generate`, the `.xcodeproj` may have a cleared signing team. Always re-check your team settings:
|
||||
|
||||
```bash
|
||||
cd tabatago-swift
|
||||
xcodegen generate
|
||||
# Then check that project.yml has the correct team ID in settings
|
||||
```
|
||||
error: Unable to find a device matching the provided destination specifier
|
||||
```
|
||||
|
||||
### Complication Targets Missing Team ID
|
||||
- Check that `IPHONE_UDID` secret matches the actual device UDID
|
||||
- The device must be registered in your Apple Developer account
|
||||
|
||||
Targets like **TabataGoWatchWidget** (watchOS complications) require an explicit team ID in `project.yml` settings. If you see signing errors for the widget target:
|
||||
### Comment not posted / LGTM not detected
|
||||
|
||||
1. Open `tabatago-swift/project.yml`
|
||||
2. Find the `TabataGoWatchWidget` target section
|
||||
3. Ensure it has an explicit `settings:` block with the team ID:
|
||||
```yaml
|
||||
target: TabataGoWatchWidget
|
||||
settings:
|
||||
base:
|
||||
DEVELOPMENT_TEAM: "2MJF39L8VY"
|
||||
```
|
||||
- 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
|
||||
|
||||
### General Build Debugging
|
||||
### Workflow runs on every PR push, canceling previous runs
|
||||
|
||||
- Run `xcodegen generate` before every build to ensure the project is in sync
|
||||
- Check `xcodebuild` output for specific file/line errors
|
||||
- Verify SPM packages resolve: `xcodebuild -resolvePackageDependencies`
|
||||
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)
|
||||
|
||||
1374
tabatago-swift/TabataGo.xcodeproj/project.pbxproj
Normal file
1374
tabatago-swift/TabataGo.xcodeproj/project.pbxproj
Normal file
File diff suppressed because it is too large
Load Diff
7
tabatago-swift/TabataGo.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
tabatago-swift/TabataGo.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"originHash" : "42d1f35b4500c2779457daf99841f2333a14d9a2965835305d89fd95beda836e",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "plcrashreporter",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/microsoft/plcrashreporter.git",
|
||||
"state" : {
|
||||
"revision" : "0254f941c646b1ed17b243654723d0f071e990d0",
|
||||
"version" : "1.12.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "posthog-ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/PostHog/posthog-ios",
|
||||
"state" : {
|
||||
"revision" : "c3efdae383a5e7a5a88c34fd774e9d7dc915b9d4",
|
||||
"version" : "3.55.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "purchases-ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/RevenueCat/purchases-ios",
|
||||
"state" : {
|
||||
"revision" : "bd63241b2258ea519020eb32a349db44fb44b119",
|
||||
"version" : "5.68.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "supabase-swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/supabase/supabase-swift",
|
||||
"state" : {
|
||||
"revision" : "17261e93c60aa721e3c17312bfeb2ae6de3d6f8a",
|
||||
"version" : "2.43.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-asn1",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-asn1.git",
|
||||
"state" : {
|
||||
"revision" : "eb50cbd14606a9161cbc5d452f18797c90ef0bab",
|
||||
"version" : "1.7.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-clocks",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-clocks",
|
||||
"state" : {
|
||||
"revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e",
|
||||
"version" : "1.0.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-concurrency-extras",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
|
||||
"state" : {
|
||||
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
|
||||
"version" : "1.3.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-crypto",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-crypto.git",
|
||||
"state" : {
|
||||
"revision" : "476538ccb827f2dd18efc5de754cc87d77127a47",
|
||||
"version" : "4.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-http-types",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-http-types.git",
|
||||
"state" : {
|
||||
"revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca",
|
||||
"version" : "1.5.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "xctest-dynamic-overlay",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
||||
"state" : {
|
||||
"revision" : "dfd70507def84cb5fb821278448a262c6ff2bbad",
|
||||
"version" : "1.9.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2640"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "92991789C3A5B2A5FACF07A1"
|
||||
BuildableName = "TabataGo.app"
|
||||
BlueprintName = "TabataGo"
|
||||
ReferencedContainer = "container:TabataGo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
queueDebuggingEnableBacktraceRecording = "Yes">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "92991789C3A5B2A5FACF07A1"
|
||||
BuildableName = "TabataGo.app"
|
||||
BlueprintName = "TabataGo"
|
||||
ReferencedContainer = "container:TabataGo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "92991789C3A5B2A5FACF07A1"
|
||||
BuildableName = "TabataGo.app"
|
||||
BlueprintName = "TabataGo"
|
||||
ReferencedContainer = "container:TabataGo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2640"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D1E2CCCC7C9BD41029959883"
|
||||
BuildableName = "TabataGoTests.xctest"
|
||||
BlueprintName = "TabataGoTests"
|
||||
ReferencedContainer = "container:TabataGo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
queueDebuggingEnableBacktraceRecording = "Yes">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,62 @@
|
||||
<?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>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>TabataGo.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>TabataGoTests.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>TabataGoUITests.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>TabataGoWatch.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>TabataGoWatchWidget.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>TabataGoWidget.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>3945C3998B4B66F30759718C</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>90BAF2DB5D7456CD45975E26</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>92991789C3A5B2A5FACF07A1</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>D1E2CCCC7C9BD41029959883</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -28,7 +28,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2</string>
|
||||
<string>1</string>
|
||||
<key>NSHealthShareUsageDescription</key>
|
||||
<string>TabataGo reads your health data to show fitness stats and personalize your workouts.</string>
|
||||
<key>NSHealthUpdateUsageDescription</key>
|
||||
|
||||
@@ -1,38 +1,30 @@
|
||||
import Foundation
|
||||
#if canImport(PostHog)
|
||||
import PostHog
|
||||
#endif
|
||||
|
||||
/// PostHog analytics — mirrors the event taxonomy from the Expo app.
|
||||
final class AnalyticsService: @unchecked Sendable {
|
||||
|
||||
static let shared = AnalyticsService()
|
||||
|
||||
#if canImport(PostHog)
|
||||
private let apiKey: String =
|
||||
Bundle.main.infoDictionary?["POSTHOG_API_KEY"] as? String ?? ""
|
||||
|
||||
private let host = "https://eu.posthog.com"
|
||||
#endif
|
||||
|
||||
private init() {}
|
||||
|
||||
func initialize() {
|
||||
#if canImport(PostHog)
|
||||
guard !apiKey.isEmpty else { return }
|
||||
let config = PostHogConfig(apiKey: apiKey, host: host)
|
||||
config.captureApplicationLifecycleEvents = true
|
||||
config.captureScreenViews = false // manual tracking
|
||||
PostHogSDK.shared.setup(config)
|
||||
#endif
|
||||
}
|
||||
|
||||
// ─── Screens ────────────────────────────────────────────────
|
||||
|
||||
func screen(_ name: String, properties: [String: Any] = [:]) {
|
||||
#if canImport(PostHog)
|
||||
PostHogSDK.shared.screen(name, properties: properties)
|
||||
#endif
|
||||
}
|
||||
|
||||
// ─── Onboarding ─────────────────────────────────────────────
|
||||
@@ -108,8 +100,6 @@ final class AnalyticsService: @unchecked Sendable {
|
||||
// ─── Private ─────────────────────────────────────────────────
|
||||
|
||||
private func capture(_ event: String, properties: [String: Any] = [:]) {
|
||||
#if canImport(PostHog)
|
||||
PostHogSDK.shared.capture(event, properties: properties.isEmpty ? nil : properties)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ struct TabataEntry: TimelineEntry {
|
||||
|
||||
struct TabataProvider: TimelineProvider {
|
||||
|
||||
private let sharedDefaults = UserDefaults(suiteName: "group.fr.millianlmx.tabatago")
|
||||
private let sharedDefaults = UserDefaults(suiteName: "group.com.tabatago.app")
|
||||
|
||||
func placeholder(in context: Context) -> TabataEntry {
|
||||
TabataEntry(date: Date(), streak: 7, lastWorkoutLabel: "Today")
|
||||
@@ -98,8 +98,6 @@ struct RectangularComplicationView: View {
|
||||
}
|
||||
|
||||
/// `.accessoryCorner` — tiny bolt + streak digit in the corner
|
||||
#if canImport(WatchKit)
|
||||
@available(watchOS 10.0, *)
|
||||
struct CornerComplicationView: View {
|
||||
let entry: TabataEntry
|
||||
|
||||
@@ -113,7 +111,6 @@ struct CornerComplicationView: View {
|
||||
.widgetLabel(String(format: String(localized: LocalizedStringResource("watch.complication.dayStreakFmt")), entry.streak))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// ─── Widget definition ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -127,11 +124,11 @@ struct TabataGoComplication: Widget {
|
||||
}
|
||||
.configurationDisplayName("TabataGo")
|
||||
.description(String(localized: LocalizedStringResource("watch.complication.description")))
|
||||
#if canImport(WatchKit)
|
||||
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryCorner])
|
||||
#else
|
||||
.supportedFamilies([.accessoryCircular, .accessoryRectangular])
|
||||
#endif
|
||||
.supportedFamilies([
|
||||
.accessoryCircular,
|
||||
.accessoryRectangular,
|
||||
.accessoryCorner
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,10 +142,8 @@ struct TabataComplicationEntryView: View {
|
||||
CircularComplicationView(entry: entry)
|
||||
case .accessoryRectangular:
|
||||
RectangularComplicationView(entry: entry)
|
||||
#if canImport(WatchKit)
|
||||
case .accessoryCorner:
|
||||
CornerComplicationView(entry: entry)
|
||||
#endif
|
||||
default:
|
||||
CircularComplicationView(entry: entry)
|
||||
}
|
||||
@@ -168,12 +163,3 @@ struct TabataComplicationEntryView: View {
|
||||
} timeline: {
|
||||
TabataEntry(date: .now, streak: 7, lastWorkoutLabel: "Today")
|
||||
}
|
||||
|
||||
#if canImport(WatchKit)
|
||||
@available(watchOS 10.0, *)
|
||||
#Preview("Corner", as: .accessoryCorner) {
|
||||
TabataGoComplication()
|
||||
} timeline: {
|
||||
TabataEntry(date: .now, streak: 7, lastWorkoutLabel: "Today")
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
<key>WKApplication</key>
|
||||
<true/>
|
||||
<key>WKCompanionAppBundleIdentifier</key>
|
||||
<string>fr.millianlmx.tabatago</string>
|
||||
<string>com.tabatago.app</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.fr.millianlmx.tabatago</string>
|
||||
<string>group.com.tabatago.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -13,7 +13,7 @@ struct WatchActivityView: View {
|
||||
@State private var isLoading = true
|
||||
|
||||
private let healthStore = HKHealthStore()
|
||||
private let sharedDefaults = UserDefaults(suiteName: "group.fr.millianlmx.tabatago")
|
||||
private let sharedDefaults = UserDefaults(suiteName: "group.com.tabatago.app")
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: TabataGo
|
||||
|
||||
options:
|
||||
bundleIdPrefix: fr.millianlmx.tabatago
|
||||
bundleIdPrefix: com.tabatago
|
||||
deploymentTarget:
|
||||
iOS: "26.0"
|
||||
watchOS: "11.0"
|
||||
@@ -15,11 +15,7 @@ settings:
|
||||
SWIFT_VERSION: "6.0"
|
||||
IPHONEOS_DEPLOYMENT_TARGET: "26.0"
|
||||
SWIFT_STRICT_CONCURRENCY: complete
|
||||
SWIFT_EMIT_MODULE_FOR_EXPLICIT_BUILD: NO
|
||||
GCC_TREAT_WARNINGS_AS_ERRORS: NO
|
||||
SWIFT_TREAT_WARNINGS_AS_ERRORS: NO
|
||||
DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
|
||||
DEVELOPMENT_TEAM: "2MJF39L8VY"
|
||||
|
||||
packages:
|
||||
Supabase:
|
||||
@@ -28,6 +24,9 @@ packages:
|
||||
RevenueCat:
|
||||
url: https://github.com/RevenueCat/purchases-ios
|
||||
from: "5.0.0"
|
||||
PostHog:
|
||||
url: https://github.com/PostHog/posthog-ios
|
||||
from: "3.0.0"
|
||||
|
||||
targets:
|
||||
TabataGo:
|
||||
@@ -67,18 +66,19 @@ targets:
|
||||
com.apple.developer.healthkit.access:
|
||||
- health-records
|
||||
com.apple.security.application-groups:
|
||||
- group.fr.millianlmx.tabatago
|
||||
- group.com.tabatago.app
|
||||
dependencies:
|
||||
- package: Supabase
|
||||
product: Supabase
|
||||
- package: RevenueCat
|
||||
product: RevenueCat
|
||||
|
||||
- package: PostHog
|
||||
product: PostHog
|
||||
- target: TabataGoWatch
|
||||
embed: true
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app
|
||||
INFOPLIST_FILE: TabataGo/Resources/Info.plist
|
||||
CODE_SIGN_ENTITLEMENTS: TabataGo/Resources/TabataGo.entitlements
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||
@@ -112,7 +112,7 @@ targets:
|
||||
CFBundleShortVersionString: "1.0"
|
||||
CFBundleVersion: "2"
|
||||
WKApplication: true
|
||||
WKCompanionAppBundleIdentifier: fr.millianlmx.tabatago
|
||||
WKCompanionAppBundleIdentifier: com.tabatago.app
|
||||
NSHealthShareUsageDescription: "TabataGo reads your heart rate and calories during workouts."
|
||||
NSHealthUpdateUsageDescription: "TabataGo saves workout data to Apple Health directly from your Watch."
|
||||
entitlements:
|
||||
@@ -120,13 +120,13 @@ targets:
|
||||
properties:
|
||||
com.apple.developer.healthkit: true
|
||||
com.apple.security.application-groups:
|
||||
- group.fr.millianlmx.tabatago
|
||||
- group.com.tabatago.app
|
||||
dependencies:
|
||||
- target: TabataGoWatchWidget
|
||||
embed: true
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago.watchkitapp
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app.watchkitapp
|
||||
INFOPLIST_FILE: TabataGoWatch/Resources/Info.plist
|
||||
CODE_SIGN_ENTITLEMENTS: TabataGoWatch/Resources/TabataGoWatch.entitlements
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||
@@ -152,7 +152,7 @@ targets:
|
||||
NSExtensionPointIdentifier: com.apple.widgetkit-extension
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago.watchkitapp.widget
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app.watchkitapp.widget
|
||||
TARGETED_DEVICE_FAMILY: "4"
|
||||
WATCHOS_DEPLOYMENT_TARGET: "11.0"
|
||||
|
||||
@@ -166,7 +166,7 @@ targets:
|
||||
- target: TabataGo
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago.tests
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app.tests
|
||||
|
||||
TabataGoUITests:
|
||||
type: bundle.ui-testing
|
||||
@@ -178,14 +178,4 @@ targets:
|
||||
- target: TabataGo
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: fr.millianlmx.tabatago.uitests
|
||||
|
||||
schemes:
|
||||
TabataGo:
|
||||
build:
|
||||
targets:
|
||||
TabataGo: all
|
||||
run:
|
||||
config: Debug
|
||||
archive:
|
||||
config: Release
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.tabatago.app.uitests
|
||||
|
||||
Reference in New Issue
Block a user