Compare commits
1 Commits
main
...
feat/zone-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a843a2bd20 |
105
.github/workflows/pr-iphone-deploy.yml
vendored
105
.github/workflows/pr-iphone-deploy.yml
vendored
@@ -8,11 +8,12 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize, reopened]
|
||||
# NOTE: no trigger-level `paths:` here — Gitea Actions does not reliably
|
||||
# honor `on.pull_request.paths:` filters (the workflow silently fails to
|
||||
# trigger even when changed files match). Path gating is done at the job
|
||||
# level via dorny/paths-filter@v3 in the `changes` job below (same pattern
|
||||
# as ci.yml). Docs/backend-only PRs then skip the macOS runner.
|
||||
# Only run the iOS build+deploy when the PR actually touches the Swift app
|
||||
# or this workflow itself. Docs-only / backend-only / admin-web-only PRs
|
||||
# skip the runner entirely (no point rebuilding an unchanged .app).
|
||||
paths:
|
||||
- 'tabatago-swift/**'
|
||||
- '.github/workflows/pr-iphone-deploy.yml'
|
||||
|
||||
# Every mutation (comments, merge) goes through the Gitea API with PR_API_TOKEN,
|
||||
# never the runner's native GITHUB_TOKEN — so a `permissions:` block would be
|
||||
@@ -25,29 +26,8 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
|
||||
# ── Path filter — determines whether the macOS build+deploy is worth running ──
|
||||
# Gitea Actions does not honor trigger-level `on.pull_request.paths:`, so we
|
||||
# gate at the job level with dorny/paths-filter (same pattern as ci.yml).
|
||||
changes:
|
||||
name: Detect Changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ios: ${{ steps.filter.outputs.ios }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
ios:
|
||||
- 'tabatago-swift/**'
|
||||
- '.github/workflows/pr-iphone-deploy.yml'
|
||||
|
||||
build-deploy:
|
||||
name: Build & Deploy to iPhone (devicectl)
|
||||
needs: changes
|
||||
if: needs.changes.outputs.ios == 'true'
|
||||
runs-on: macos
|
||||
timeout-minutes: 30
|
||||
|
||||
@@ -192,38 +172,19 @@ jobs:
|
||||
run: |
|
||||
PR="${{ github.event.pull_request.number }}"
|
||||
REPO="${{ github.repository }}"
|
||||
# The HTML comment is invisible in Gitea's rendered markdown but is
|
||||
# present in the raw body — wait-approval uses it as a sentinel to
|
||||
# skip THIS bot comment when scanning for LGTM/KO (otherwise the
|
||||
# instruction text "Reply LGTM pour merger" would self-trigger a
|
||||
# merge ~30s after deploy). DO NOT remove or reword without also
|
||||
# updating the scanner in the wait-approval job.
|
||||
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 (devicectl).\\n\\n<!-- tabatago:ready-to-test -->\\n\\n- Teste les changements\\n- Reply **LGTM** pour merger\\n- Reply **KO** pour bloquer\"}" \
|
||||
-d "{\"body\":\"## 📱 Prêt à tester !\\n\\nL'app est déployée sur l'iPhone (devicectl).\\n\\n- Teste les changements\\n- Reply **LGTM** pour merger\\n- Reply **KO** pour bloquer\"}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/issues/${PR}/comments"
|
||||
|
||||
wait-approval:
|
||||
name: Wait for LGTM comment
|
||||
needs: build-deploy
|
||||
# Only run when build-deploy actually deployed. Without this, a skipped
|
||||
# build-deploy (filtered out by `changes`) would still launch this job and
|
||||
# the merge poll would run against a PR that was never deployed to device.
|
||||
if: needs.build-deploy.result == 'success'
|
||||
runs-on: macos
|
||||
timeout-minutes: 120
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
# Shallow clone — wait-approval only needs scripts/pr_lgtm_scanner.py,
|
||||
# nothing else from the repo. Same raw-git pattern as build-deploy's
|
||||
# Checkout step (avoids actions/checkout setup).
|
||||
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 }}
|
||||
|
||||
- name: Poll for LGTM
|
||||
env:
|
||||
GT_TOKEN: ${{ secrets.PR_API_TOKEN }}
|
||||
@@ -241,19 +202,6 @@ jobs:
|
||||
# Re-fetch ALL comments each cycle (not since_id) so we also catch
|
||||
# edits — e.g. a reviewer changing "KO" → "LGTM". Negligible cost for
|
||||
# PRs with <50 comments.
|
||||
#
|
||||
# The LGTM/KO decision is computed by scripts/pr_lgtm_scanner.py
|
||||
# (a standalone Python file) rather than grep over raw JSON:
|
||||
# - the bot's "Ready to test" comment body literally contains the
|
||||
# words LGTM/KO as instructions to the reviewer, so any naive
|
||||
# body grep would self-match and auto-merge ~30s after deploy.
|
||||
# We skip it via the <!-- tabatago:ready-to-test --> sentinel
|
||||
# embedded by the build-deploy job;
|
||||
# - json.loads avoids false matches when the literal "body" key or
|
||||
# trigger words appear inside another string field.
|
||||
# KO is checked BEFORE LGTM so a PR with both signals blocks (matches
|
||||
# the existing "KO blocks" intent — a reviewer who flip-flops shouldn't
|
||||
# merge just because an older LGTM is still in the history).
|
||||
|
||||
while [ $TRIES -lt $MAX ]; do
|
||||
sleep 30
|
||||
@@ -262,30 +210,23 @@ jobs:
|
||||
COMMENTS=$(curl -s -H "Authorization: token ${GT_TOKEN}" \
|
||||
"${API}/issues/${PR}/comments?limit=50&page=1")
|
||||
|
||||
# The LGTM/KO scanner lives in scripts/pr_lgtm_scanner.py rather
|
||||
# than inline here. Embedding multi-line Python in a YAML `run: |`
|
||||
# block scalar is fragile — YAML dedents block-scalar content to
|
||||
# the first line, which produced both YAML parse failures and
|
||||
# Python IndentationErrors in earlier iterations of this workflow.
|
||||
# A standalone script sidesteps all of that and is testable locally.
|
||||
DECISION=$(python3 scripts/pr_lgtm_scanner.py "$COMMENTS")
|
||||
# Match LGTM/KO anywhere in the body (with a word boundary so "KOM"
|
||||
# or "LGTMX" don't trigger). Case-insensitive.
|
||||
if echo "$COMMENTS" | grep -qiE '"body":[[:space:]]*"[^"]*\bLGTM\b'; then
|
||||
echo "✅ LGTM reçu ! Squash-merge..."
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GT_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"Do\":\"squash\"}" \
|
||||
"${API}/pulls/${PR}/merge"
|
||||
echo "✅ Mergée (squash)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$DECISION" in
|
||||
LGTM)
|
||||
echo "✅ LGTM reçu ! Squash-merge..."
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GT_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"Do\":\"squash\"}" \
|
||||
"${API}/pulls/${PR}/merge"
|
||||
echo "✅ Mergée (squash)."
|
||||
exit 0
|
||||
;;
|
||||
KO)
|
||||
echo "❌ KO reçu. Bloquée."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
if echo "$COMMENTS" | grep -qiE '"body":[[:space:]]*"[^"]*\bKO\b'; then
|
||||
echo "❌ KO reçu. Bloquée."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ $((TRIES % 4)) -eq 0 ]; then
|
||||
echo " ⏳ ... (${TRIES}/240, $(date +%H:%M))"
|
||||
|
||||
@@ -167,7 +167,7 @@ Complications : `TabataGoComplication`.
|
||||
6. **xcodegen generate** → `xcodebuild -resolvePackageDependencies` → **build** (`-scheme TabataGo`, Debug, auto-provisioning, team `2MJF39L8VY`). Le scheme build les 4 targets : `TabataGo` (app iOS), `TabataGoWidget` (widget iOS, vrai target — ne pas confondre avec `TabataGoWatchWidget` qui est watchOS), `TabataGoWatch` (app watchOS), `TabataGoWatchWidget` (complication watchOS).
|
||||
7. **Deploy iPhone** UDID `00008120-000925CE3672201E` : `xcrun devicectl device install app` uniquement. `devicectl` gère nativement la découverte WiFi (réseau) et filaire (USB-C/Thunderbolt) — pas de fallback `ios-deploy`.
|
||||
8. **Post comment** "Prêt à tester" sur la PR.
|
||||
9. **Job `wait-approval`** : poll (30s, max 240 = 2h). **Re-fetche TOUS les comments** chaque cycle (pas `since_id`) pour attraper aussi les edits (un reviewer passant de "KO" à "LGTM"). Le parsing se fait en **python3** (`json.loads` + regex, pas de `grep` sur JSON brut). **Le commentaire bot "Prêt à tester" porte un marker sentinel `<!-- tabatago:ready-to-test -->`** et est **ignoré** par le scanner — sinon son propre body (qui mentionne LGTM/KO comme instructions au reviewer) déclencherait un auto-merge ~30s après le deploy (bug historique). KO est checké **avant** LGTM (un PR avec les deux signaux bloque). 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.
|
||||
9. **Job `wait-approval`** : poll (30s, max 240 = 2h). **Re-fetche TOUS les comments** chaque cycle (pas `since_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
|
||||
|
||||
@@ -187,7 +187,6 @@ Complications : `TabataGoComplication`.
|
||||
- `-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 à un `grep '"body": *"LGTM"'` ancré — il raterait "Tested, LGTM!".
|
||||
- **Self-match sentinel** : le commentaire bot "Prêt à tester" contient littéralement `Reply **LGTM** pour merger` et `Reply **KO**` — sans précaution, le scanner matche son **propre** commentaire et auto-merge ~30s après le deploy. Le body porte donc un marker `<!-- tabatago:ready-to-test -->` que `wait-approval` ignore. **Tout commentaire bot posté par ce workflow doit porter ce marker** ; ne pas le retirer ni poster d'autre commentaire contenant LGTM/KO sans marker.
|
||||
- **Squash-merge** (`{"Do":"squash"}`) — pas merge commit ni rebase. Garde l'historique `main` linéaire.
|
||||
- **Comment edits** : `wait-approval` re-fetche tous les comments chaque cycle (pas `since_id`) pour attraper les edits.
|
||||
|
||||
@@ -250,8 +249,6 @@ Node.js (`server.js`, `package.json`, `Dockerfile`). Télécharge l'audio de pla
|
||||
| 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!") |
|
||||
| Poster un commentaire bot dont le body contient "LGTM"/"KO" sans le marker `<!-- tabatago:ready-to-test -->` | Le scanner bot-scan ignore les comments portant le marker — tout commentaire bot du workflow doit l'inclure (sinon auto-merge en ~30s) |
|
||||
| Scanner les comments en `grep` sur le JSON brut | `python3` + `json.loads` (gère unicode/quotes, pas de false match sur la clé `"body"`) |
|
||||
| `{"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 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "my-app",
|
||||
"version": "1.1.0",
|
||||
"version": "1.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""LGTM/KO scanner for the pr-iphone-deploy workflow's wait-approval job.
|
||||
|
||||
Reads a Gitea PR comments JSON array (as returned by
|
||||
``/api/v1/repos/{owner}/{repo}/issues/{pr}/comments``) on argv[1] and prints
|
||||
exactly one of: ``LGTM``, ``KO``, ``PENDING``.
|
||||
|
||||
Rules
|
||||
-----
|
||||
* The bot's own "Ready to test" comment body literally contains the words
|
||||
``LGTM`` and ``KO`` as instructions to the reviewer — without filtering it
|
||||
out, the workflow would self-merge ~30s after deploy. Bot comments are
|
||||
tagged with the sentinel ``<!-- tabatago:ready-to-test -->`` and skipped.
|
||||
* ``KO`` is checked BEFORE ``LGTM`` — a PR with both signals blocks (matches
|
||||
the existing "KO blocks" intent).
|
||||
* Word-boundary regex (``\\bLGTM\\b`` / ``\\bKO\\b``, case-insensitive) so
|
||||
"Tested, LGTM!" counts and "KOM" / "LGTMX" do not.
|
||||
|
||||
This script lives in ``scripts/`` (not inline in the workflow) because
|
||||
embedding multi-line Python inside a YAML ``run: |`` block scalar is fragile:
|
||||
YAML dedents block-scalar content to the first line, which produces either a
|
||||
YAML parse failure or a Python ``IndentationError`` depending on how the
|
||||
body is indented. A standalone file sidesteps all of that.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
MARKER = "<!-- tabatago:ready-to-test -->"
|
||||
LGTM_RE = re.compile(r"\bLGTM\b", re.IGNORECASE)
|
||||
KO_RE = re.compile(r"\bKO\b", re.IGNORECASE)
|
||||
|
||||
|
||||
def decide(comments_json: str) -> str:
|
||||
"""Return 'KO', 'LGTM', or 'PENDING' for the given comments JSON payload."""
|
||||
try:
|
||||
comments = json.loads(comments_json or "[]")
|
||||
except Exception:
|
||||
comments = []
|
||||
|
||||
hit_lgtm = False
|
||||
hit_ko = False
|
||||
for comment in comments:
|
||||
body = comment.get("body") or ""
|
||||
if MARKER in body:
|
||||
# Skip the bot's own "Ready to test" comment — its body mentions
|
||||
# LGTM/KO as instructions and would otherwise self-trigger.
|
||||
continue
|
||||
if KO_RE.search(body):
|
||||
hit_ko = True
|
||||
if LGTM_RE.search(body):
|
||||
hit_lgtm = True
|
||||
|
||||
if hit_ko:
|
||||
return "KO"
|
||||
if hit_lgtm:
|
||||
return "LGTM"
|
||||
return "PENDING"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 2:
|
||||
print("usage: pr_lgtm_scanner.py <comments_json>", file=sys.stderr)
|
||||
return 2
|
||||
print(decide(sys.argv[1]))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "1.1.0"
|
||||
"version": "1.0.1"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user