#!/usr/bin/env bash # ============================================================================ # check_mobile_workflow.sh · OTO Enterprise OS DTP # ---------------------------------------------------------------------------- # Intégrité du SECOND workflow Gitea Actions : `.gitea/workflows/mobile-build.yml` # (EAS build iOS/Android · Sprint 5 l.56 · DIRECTIVE_MOBILE_STORES_20260803). # # POURQUOI ce gate. `mobile-build.yml` est HORS `gate.needs` (aucun de ses jobs # ne conditionne un merge — il s'active seulement quand Michel fournit # `EAS_TOKEN`). Conséquence : AUCUN gate ne lintait sa structure — # `check_ci_integrity.sh` ne verrouille QUE `ci.yml`, `validate_json.sh` ne # couvre que le `.json`. Une régression y passait donc VERTE en silence (mutation # vérifiée : réintroduire `secrets.` dans un `if:` de job → 7/7 gates statiques # restaient verts). Or ce workflow porte un correctif de PORTABILITÉ subtil # (session 234104) qui, s'il régresse, ne casse RIEN au CI mais fait échouer les # builds « le jour où on en a besoin » — exactement le « marche dans ma tête, # casse en prod » que la série anti-dérive traque. Ce gate rend ces invariants # opposables, en lisant des FAITS dans le fichier (jamais une liste à la main). # # INVARIANTS (tous re-dérivés du fichier · #6) : # [MOB-1] Bien-formé : le fichier existe, porte `name:`/`on:`/`jobs:` et les # 3 jobs attendus (preflight, build-ios, build-android). Si PyYAML est # présent, le parse YAML strict est EXIGÉ ; sinon contrôle structurel # awk seul (runner Gitea sans pip → best-effort, jamais un faux vert : # les invariants portants MOB-2/3/4 ne dépendent pas de PyYAML). # [MOB-2] Portabilité du gating (le correctif 234104) : AUCUN `if:` au niveau # JOB ne référence le contexte `secrets.` — non exposé dans # `jobs..if` par GitHub Actions ; sur `act`/Gitea il s'évalue vide # → le job resterait SKIP en silence même EAS_TOKEN fourni. # [MOB-3] Gating d'activation préservé (#6/#8) : chaque job de BUILD (bloc qui # contient `eas build`) porte un `if:` job-level gaté sur # `needs.preflight.outputs.has_token` → SKIP tant que le token absent, # donc le workflow ne rend JAMAIS le CI rouge avant activation Michel. # [MOB-4] Contrat d'outputs intact : `preflight` déclare les outputs # (`has_token`, `has_repo`), chaque `needs.preflight.outputs.` cité # dans un `if:` est bien déclaré (0 dangling → pas de SKIP par output # fantôme), et chaque output déclaré est ALIMENTÉ par un # `echo "=… >> $GITHUB_OUTPUT"` dans un step. # # Dépendances : bash, git, awk, grep, sort. python3+PyYAML utilisé si présent # (best-effort). Aucun réseau (CLAUDE.md #2 · runner Gitea sans pip). # ============================================================================ set -uo pipefail # shellcheck source=ci/lib.sh source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" || exit 3 cd_repo_root # racine du dépôt, ou ROUGE honnête si hors arbre git (cf. lib.sh) WF=".gitea/workflows/mobile-build.yml" FAIL=0 report() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; } ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; } if [[ ! -f "$WF" ]]; then report "workflow mobile introuvable : $WF" echo -e "\n\033[31m❌ Mobile workflow absent.\033[0m"; exit 1 fi # --- Extraction (awk · même philosophie de parseur minimal que check_ci_integrity) # Un job = clé `^ :` à 2 espaces exactement, sous la ligne `jobs:`. JOBS="$(awk ' /^jobs:[[:space:]]*$/ { injobs=1; next } injobs && match($0, /^ ([A-Za-z0-9_-]+):[[:space:]]*$/, m) { print m[1] } ' "$WF" | sort)" # if: au niveau JOB = ligne indentée à EXACTEMENT 4 espaces ` if:` (un if: de # step est plus profond ; un secrets. en `env:`/step est légitime — hors scope). JOB_IF_SECRETS="$(grep -nE '^ if:.*secrets\.' "$WF" || true)" # Jobs de BUILD = ceux dont le bloc lance une commande `run: eas build` (une # mention `eas build` en COMMENTAIRE ne compte pas — sinon un `# … eas build …` # placé entre deux jobs serait attribué au job précédent). BUILD_JOBS="$(awk ' /^jobs:[[:space:]]*$/ { injobs=1; next } injobs && match($0, /^ ([A-Za-z0-9_-]+):[[:space:]]*$/, m) { job=m[1]; next } injobs && job != "" && /run:[[:space:]]*eas build/ { print job } ' "$WF" | sort -u)" # Pour chaque job, sa (ses) ligne(s) if: job-level agrégée(s). job_if() { # $1 = nom du job → imprime le contenu de son if: job-level awk -v want="$1" ' /^jobs:[[:space:]]*$/ { injobs=1; next } injobs && match($0, /^ ([A-Za-z0-9_-]+):[[:space:]]*$/, m) { job=m[1]; next } injobs && job==want && match($0, /^ if:[[:space:]]*(.+)$/, m) { print m[1] } ' "$WF" } # --- MOB-1 : bien-formé ------------------------------------------------------ echo "== MOB-1 · $WF bien-formé (name/on/jobs + 3 jobs attendus) ==" for key in 'name:' 'on:' 'jobs:'; do grep -qE "^${key}" "$WF" || report "clé top-level manquante : ${key%:}" done for j in preflight build-ios build-android; do printf '%s\n' "$JOBS" | grep -qx "$j" || report "job attendu absent : $j" done # Parse YAML strict SI PyYAML dispo (best-effort · runner sans pip → info seule). if python3 -c 'import yaml' 2>/dev/null; then if python3 -c 'import sys,yaml; yaml.safe_load(open(sys.argv[1]))' "$WF" 2>/tmp/mobyaml; then ok "YAML strict valide (PyYAML) · jobs = [$(printf '%s' "$JOBS" | paste -sd, -)]" else report "YAML invalide (PyYAML) : $(tr '\n' ' ' n'importe où. REFERENCED="$(grep -oE 'needs\.preflight\.outputs\.[A-Za-z0-9_]+' "$WF" | sed 's/.*outputs\.//' | sort -u)" # Outputs ALIMENTÉS via echo "=… >> $GITHUB_OUTPUT" dans un step. FED="$(grep -oE '(^|[^A-Za-z0-9_])[A-Za-z0-9_]+=.*>> *"?\$GITHUB_OUTPUT' "$WF" \ | grep -oE '[A-Za-z0-9_]+=' | sed 's/=$//' | sort -u)" for x in has_token has_repo; do printf '%s\n' "$DECLARED" | grep -qx "$x" || report "output attendu non déclaré dans preflight.outputs : $x" done # Chaque output référencé doit être déclaré (0 dangling → pas de SKIP fantôme). dangling="$(comm -23 <(printf '%s\n' "$REFERENCED") <(printf '%s\n' "$DECLARED"))" for x in $dangling; do report "needs.preflight.outputs.$x référencé mais NON déclaré (output fantôme → if: toujours faux)" done # Chaque output déclaré doit être alimenté par un step (sinon toujours vide). unfed="$(comm -23 <(printf '%s\n' "$DECLARED") <(printf '%s\n' "$FED"))" for x in $unfed; do report "output '$x' déclaré mais jamais écrit dans \$GITHUB_OUTPUT (resterait vide)" done if [[ -z "$dangling$unfed" ]] && printf '%s\n' "$DECLARED" | grep -qx has_token; then ok "outputs [$(printf '%s' "$DECLARED" | paste -sd, -)] déclarés · référencés · alimentés (0 dangling)" fi echo if [[ "$FAIL" -eq 0 ]]; then echo -e "\033[32m✅ mobile-build.yml intègre (gating portable · activation différée préservée).\033[0m" else echo -e "\033[31m❌ Dérive mobile-build.yml — corriger avant merge (risque de SKIP silencieux des builds).\033[0m" fi exit "$FAIL"