From c1cbc0282622f5bfda1a219219341432b44d5c52 Mon Sep 17 00:00:00 2001 From: millianlmx Date: Fri, 26 Jun 2026 20:42:58 +0000 Subject: [PATCH] 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 --- .gitea/workflows/pr-iphone-deploy.yml | 71 ++++--- docs/ci-cd-setup.md | 294 ++++++++++++++++++++++++++ 2 files changed, 330 insertions(+), 35 deletions(-) create mode 100644 docs/ci-cd-setup.md diff --git a/.gitea/workflows/pr-iphone-deploy.yml b/.gitea/workflows/pr-iphone-deploy.yml index 7258302..eabe265 100644 --- a/.gitea/workflows/pr-iphone-deploy.yml +++ b/.gitea/workflows/pr-iphone-deploy.yml @@ -55,14 +55,15 @@ jobs: cd tabatago-swift xcodegen generate - - name: Build & Install on iPhone + - 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 output while still streaming to stdout + # Use tee to capture full log while showing last 40 lines on stdout + set -o pipefail xcodebuild build \ -scheme TabataGo \ -sdk iphoneos \ @@ -74,7 +75,7 @@ jobs: CODE_SIGN_STYLE=Automatic \ 2>&1 | tee ../build/xcodebuild.log | tail -40 - BUILD_EXIT=${PIPESTATUS[0]} + BUILD_EXIT=$? if [ $BUILD_EXIT -ne 0 ]; then echo "" echo "❌ Build FAILED (exit code: $BUILD_EXIT)" @@ -95,7 +96,7 @@ jobs: REPO="${{ github.repository }}" RESP=$(curl -sk -w "\n%{http_code}" -X POST \ - -H "Authorization: token $GITEA_TOKEN" \ + -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") @@ -136,7 +137,7 @@ jobs: # RΓ©cupΓ©rer le dernier ID de commentaire existant comme point de dΓ©part LAST_ID=$(curl -sk \ - -H "Authorization: token ${GITEA_TOKEN}" \ + -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) @@ -150,7 +151,7 @@ jobs: # RΓ©cupΓ©rer tous les commentaires COMMENTS=$(curl -sk \ - -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Authorization: token $GITEA_TOKEN \ "${API}/issues/${PR_NUMBER}/comments" 2>/dev/null) if [ -z "$COMMENTS" ]; then @@ -159,12 +160,12 @@ jobs: # 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) + 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 @@ -176,28 +177,28 @@ print(json.dumps(new)) # 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") + 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 + 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 "Authorization: token $GITEA_TOKEN \ -H "Content-Type: application/json" \ - -d "{\"Do\":\"merge\",\"MergeTitle\":\"${{ github.event.pull_request.title }}\"}" \ + -d "{\"Do\":\"merge\"}" \ "${API}/pulls/${PR_NUMBER}/merge") MERGE_HTTP=$(echo "$MERGE_RESP" | tail -1) @@ -214,14 +215,14 @@ for c in cs: # 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 + 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 diff --git a/docs/ci-cd-setup.md b/docs/ci-cd-setup.md new file mode 100644 index 0000000..cd17bc1 --- /dev/null +++ b/docs/ci-cd-setup.md @@ -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 \ + --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 + + + + + + Label + com.tabatago.runner + ProgramArguments + + /path/to/act_runner + daemon + + WorkingDirectory + /path/to/runner + RunAtLoad + + KeepAlive + + + + ``` + + 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)