[DTP-Worker 20260805_001109] Nouveau gate check-mobile-workflow : la structure de mobile-build.yml devient opposable au merge (surface mutation-testée UNGATED, 1re couverture) → 32→33 jobs
- ci/check_mobile_workflow.sh (8e gate statique) · 4 invariants lus dans le fichier (#6) : MOB-1 bien-formé (YAML strict si PyYAML) · MOB-2 aucun secrets. en if: de job (portabilité act/Gitea, verrouille le correctif 234104) · MOB-3 builds gatés sur needs.preflight.outputs.has_token (activation différée #6/#8) · MOB-4 contrat outputs (déclarés==référencés==alimentés, 0 dangling). - Preuve UNGATED avant ajout : réintroduire secrets. en if: de job → 7/7 gates statiques verts. - Gate mutation-testé : 5 mutations toutes ROUGES, fichier propre re-VERT. - Câblé dans ci.yml + gate.needs ; check-ci-integrity INV-A/B verts ; run_ci auto → 33 PASS. - ci/README.md : ligne §1 + détail §2 + nuance « Second workflow ». - 0 nouveau module · 0 chiffre inventé · 0 edition prod hors CI · aucune commande VPS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+17
-1
@@ -117,6 +117,22 @@ jobs:
|
||||
- name: Contrôle des chiffres du README
|
||||
run: bash ci/check_readme_claims.sh
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Intégrité du SECOND workflow `mobile-build.yml` (Sprint 5 mobile · hors gate
|
||||
# de merge, mais AUCUN gate ne lintait sa structure : check-ci-integrity ne
|
||||
# verrouille que ci.yml, validate-json que le .json). Ce gate rend opposable le
|
||||
# correctif de portabilité 234104 (pas de `secrets.` en if: de job → SKIP
|
||||
# silencieux) + l'activation différée #6/#8 (builds gatés sur les outputs de
|
||||
# preflight). stdlib + awk pur, PyYAML best-effort. Détail : ci/README.md §2.
|
||||
# --------------------------------------------------------------------------
|
||||
check-mobile-workflow:
|
||||
name: Intégrité mobile-build.yml (gating portable · activation différée · #6/#8)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Contrôle structure mobile-build.yml
|
||||
run: bash ci/check_mobile_workflow.sh
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Publiciste (Sprint 2) : parser faisabilité → projets_master.json + generator.
|
||||
# Tests unitaires stdlib pur (unittest) → aucune installation pip requise sur
|
||||
@@ -566,7 +582,7 @@ jobs:
|
||||
gate:
|
||||
name: Gate qualité (agrégat)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [constraints-guard, validate-json, check-docs, check-artifacts, check-regression, check-ci-integrity, check-readme-claims, publiciste-tests, rbac-tests, faisabilite-gen-tests, rbac-fixtures-tests, rbac-userperm-tests, rbac-roleprofile-tests, rbac-applyplan-tests, bancable-tests, crm-workflow-vente-tests, crm-dossier-vente-tests, crm-commissions-tests, crm-financement-bancaire-tests, fiscal-ecf-tests, frontend-portails-tests, legal-confotur-tests, qa-audit-5d-tests, seo-tests, chat-otoia-tests, qa-audit-4big-tests, demo-scenario-tests, qa-regression-tests, devops-deploy-runbook-tests, qa-acceptance-tests, mobile-app-config-tests, pie-manifest-tests]
|
||||
needs: [constraints-guard, validate-json, check-docs, check-artifacts, check-regression, check-ci-integrity, check-readme-claims, check-mobile-workflow, publiciste-tests, rbac-tests, faisabilite-gen-tests, rbac-fixtures-tests, rbac-userperm-tests, rbac-roleprofile-tests, rbac-applyplan-tests, bancable-tests, crm-workflow-vente-tests, crm-dossier-vente-tests, crm-commissions-tests, crm-financement-bancaire-tests, fiscal-ecf-tests, frontend-portails-tests, legal-confotur-tests, qa-audit-5d-tests, seo-tests, chat-otoia-tests, qa-audit-4big-tests, demo-scenario-tests, qa-regression-tests, devops-deploy-runbook-tests, qa-acceptance-tests, mobile-app-config-tests, pie-manifest-tests]
|
||||
steps:
|
||||
- name: Résultat
|
||||
run: echo "✅ Gate qualité 4Big franchi — tous les checks verts."
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Activity Log · 2026-08-05 · Claude Code DTP Worker
|
||||
|
||||
## Session 001109 · Nouveau gate `check-mobile-workflow` — la structure de `mobile-build.yml` devient opposable au merge (surface mutation-testée UNGATED, 1er contrôle) → 1 gate ajouté + câblé · 32→33 jobs
|
||||
|
||||
**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **32 PASS · 0 FAIL · 0
|
||||
SKIP**, arbre propre. Roadmap `ROADMAP_8_WEEKS_OR_LESS.md` intégralement livrée/gatée
|
||||
(8 sprints). Les 4 sessions du 08-04 (`221055`, `224101`, `231101`, `234104`) avaient
|
||||
saturé les surfaces d'audit read-only (inter-artefacts, dérive non-gatée, attestation
|
||||
stakeholder) puis **durci `mobile-build.yml`** (pattern preflight-output, portable
|
||||
GitHub/Gitea). Cette dernière édition a laissé une **queue** : son propre log notait
|
||||
« Aucun gate ne linte la structure de `mobile-build.yml` (`check-ci-integrity` ne
|
||||
verrouille QUE `ci.yml`) ». Autrement dit le correctif de portabilité qu'elle venait
|
||||
de poser **pouvait régresser en silence** — exactement la classe « marche dans ma
|
||||
tête, casse le jour où on en a besoin » (mémoire [[mobile-build-secrets-if-gating]] :
|
||||
« workflow HORS `gate.needs` → `run_ci` won't catch a regression »). La valeur honnête
|
||||
du jour : **fermer cette queue** en rendant la structure de `mobile-build.yml`
|
||||
opposable au merge. NON redondant (#5) : c'est le **premier** contrôle de ce fichier.
|
||||
|
||||
**Preuve d'absence de couverture AVANT d'ajouter le gate** (discipline
|
||||
[[verify-uncovered-before-gating]]). Mutation : réintroduire l'exact anti-pattern
|
||||
corrigé en `234104` — `secrets.EAS_TOKEN != ''` dans un `if:` au niveau job. Résultat :
|
||||
**7/7 gates statiques restent VERTS**. Surface confirmée non gatée (ni
|
||||
`check_ci_integrity` — scope `CI=ci.yml` — ni `validate_json` — scope `.json`). Fichier
|
||||
byte-restauré aussitôt (`cp` depuis backup tmp · jamais `git clean`). <!-- ci-allow : mention de l'interdit, jamais exécuté -->
|
||||
|
||||
**Livrable : `ci/check_mobile_workflow.sh` (8ᵉ gate statique) + câblage.** Le gate lit
|
||||
des **FAITS dans le fichier** (jamais une liste à la main · #6), 4 invariants :
|
||||
- **MOB-1 · bien-formé** — existe · `name:`/`on:`/`jobs:` + 3 jobs (`preflight`,
|
||||
`build-ios`, `build-android`) ; parse YAML strict **exigé si PyYAML dispo**, sinon
|
||||
awk seul (runner sans pip → best-effort, jamais un faux vert : MOB-2/3/4 en sont
|
||||
indépendants).
|
||||
- **MOB-2 · portabilité** — **0** `secrets.` dans un `if:` de **job** (`^ if:`).
|
||||
Le contexte `secrets` n'est pas exposé dans `jobs.<id>.if` (table d'availability
|
||||
GitHub Actions) → sur `act`/Gitea il s'évaluerait vide → **SKIP silencieux**. C'est
|
||||
la régression `234104` verrouillée.
|
||||
- **MOB-3 · activation différée (#6/#8)** — chaque job de build (`run: eas build`) est
|
||||
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** — `preflight` déclare `has_token`/`has_repo`, chaque
|
||||
`needs.preflight.outputs.<X>` référencé est **déclaré** (0 dangling) et chaque output
|
||||
déclaré est **alimenté** par un `echo …>>$GITHUB_OUTPUT`.
|
||||
|
||||
**Bug corrigé pendant l'écriture (auto-vérification).** 1er jet : `preflight` était
|
||||
classé « job de build » car un **commentaire** `# … eas build …` (l.108, entre deux
|
||||
jobs) était attribué au job précédent par le parseur awk. Corrigé en exigeant
|
||||
`run:[[:space:]]*eas build` (commande réelle, pas une mention). Après correctif : gate
|
||||
**VERT** sur le fichier propre.
|
||||
|
||||
**Mutation-testing du gate lui-même (5 mutations · toutes ROUGES, fichier propre
|
||||
re-VERT).** (1) YAML cassé (indent) · (2) `secrets.` en `if:` de job · (3) `if:` du
|
||||
job `build-ios` supprimé · (4) output fantôme référencé (`has_tokenX`) · (5) output
|
||||
`has_repo` non alimenté. Chaque mutation → `exit 1` ; `cp` backup → gate re-VERT.
|
||||
|
||||
**Câblage (garde `check-ci-integrity` INV-A/B vert).** Job `check-mobile-workflow`
|
||||
ajouté à `ci.yml` (steps `bash ci/check_mobile_workflow.sh`) **et** à `gate.needs`.
|
||||
`check_ci_integrity` reconnaît le nouveau `ci/*.sh` comme **câblé** (lancé par un job
|
||||
du gate + il source `lib.sh`) → INV-A (gate.needs == tous jobs non-manuels) et INV-B
|
||||
(tout `ci/*.sh` câblé) **restent verts**. `run_ci.sh` (dérivé de `gate.needs`, sans
|
||||
liste en dur) l'inclut **automatiquement** → passage **32→33 jobs (8 gates statiques
|
||||
+ 25 suites)**, zéro édition de `run_ci.sh` (#6, exactitude structurelle).
|
||||
|
||||
**Doc (traçabilité, pas de dérive).** `ci/README.md` : ligne §1 pour
|
||||
`check-mobile-workflow`, sous-section §2 détaillée (4 invariants + les 5 mutations),
|
||||
et nuance ajoutée à « Second workflow » (les **jobs** restent hors gate de merge, mais
|
||||
la **structure du fichier** est désormais gatée). Convention [[ci-readme-table-detail-in-section2]]
|
||||
respectée : ligne §1 courte, détail en §2. Les mentions historiques « 7 gates
|
||||
re-verts » (§2, records de mutations passées) restent exactes en **passé** ; le nouveau
|
||||
record dit **8ᵉ gate**.
|
||||
|
||||
**Vérifications.** `bash ci/check_mobile_workflow.sh` → VERT (4 invariants) ; 5
|
||||
mutations → ROUGE ; `python3 yaml.safe_load` sur `ci.yml` → valide ; `check_ci_integrity`
|
||||
→ VERT (câblage intègre) ; `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**. **1 gate
|
||||
ajouté (NON redondant : 1re couverture d'un fichier mutation-testé ungated · pas un
|
||||
doublon #5) · 1 job CI câblé · 0 nouveau module · 0 chiffre inventé (#6) · 0 édition
|
||||
de production hors CI · aucune commande VPS (#8) · aucun `git clean`.** <!-- ci-allow : mention de l'interdit, jamais exécuté -->
|
||||
@@ -21,6 +21,7 @@ et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` pa
|
||||
| `check-regression` | `ci/check_regression.sh` | Fraîcheur : `qa/regression/out/regression_run.json` (`run`) == run frais + verdict PASS | ✅ oui |
|
||||
| `check-ci-integrity` | `ci/check_ci_integrity.sh` | Intégrité du câblage : `gate.needs` == tous les jobs non-manuels · chaque `ci/*.sh` câblé au gate | ✅ oui |
|
||||
| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres **et des faits data-derived** des docs d'entrée (`README.md` « État courant », fiches `03_agents/*/AGENT.md`, READMEs de module) : chaque nombre, label de verdict et identité citée en prose (rôle · DocType · entité · marque · persona…) est **recomputé de l'artefact commité qu'il cite** — ou re-dérivé de `CLAUDE.md` / de la roadmap pour les valeurs ancrées au mandat — puis exigé **exact** (#6), jamais une liste à la main : un doc qui se contredit avec sa propre source est un « vert trompeur ». Surfaces couvertes : comptes de tests/suites, verdicts & barème 4Big, ledgers d'invariants CI, ventilations & identités RBAC, mots-clés & graphe SEO, pipeline vente, e-CF DGII, CONFOTUR, app mobile Expo, run-books VPS, tokens design #4, etc. **Détail exhaustif surface par surface : §2 `check_readme_claims.sh` (ci-dessous).** | ✅ oui |
|
||||
| `check-mobile-workflow` | `ci/check_mobile_workflow.sh` | Intégrité du **second** workflow `mobile-build.yml` (hors gate de merge, mais sa STRUCTURE est gatée ici) : gating portable (0 `secrets.` en `if:` de job → pas de SKIP silencieux) · builds gatés sur les outputs `preflight` (activation différée #6/#8) · contrat d'outputs (déclarés == référencés == alimentés). **Détail : §2 `check_mobile_workflow.sh`.** | ✅ oui |
|
||||
| `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui |
|
||||
|
||||
Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec
|
||||
@@ -41,6 +42,17 @@ il ne rend **jamais** le CI rouge (`preflight` reste vert et rapporte les secret
|
||||
attendus). Périmètre complet (#8 · exécution serveur hors repo), profils EAS et
|
||||
variables `a_confirmer` : [`README` du module Mobile](../05_deliverables_mvp/mobile/app_config/README.md).
|
||||
|
||||
Nuance importante : ses **jobs** sont hors gate de merge, mais sa **structure de
|
||||
fichier**, elle, EST gatée — par le job `check-mobile-workflow`
|
||||
([`ci/check_mobile_workflow.sh`](check_mobile_workflow.sh), dans `gate.needs`).
|
||||
Sans lui, une régression du workflow passait VERTE en silence (mutation vérifiée :
|
||||
réintroduire `secrets.` dans un `if:` de job laissait les 7 gates statiques verts).
|
||||
C'est précisément le risque « marche dans ma tête, casse le jour où Michel active
|
||||
`EAS_TOKEN` » : le contexte `secrets` n'est pas disponible dans `jobs.<id>.if`
|
||||
(table d'availability GitHub Actions), donc un tel `if:` s'évaluerait vide sur
|
||||
`act`/Gitea → build **SKIP silencieux** même token fourni. Le gate rend cet
|
||||
invariant (et l'activation différée) opposable au merge.
|
||||
|
||||
### Rejouer le gate EN LOCAL — `run_ci.sh` (racine du dépôt)
|
||||
|
||||
Avant de committer, reproduire l'intégralité du gate de merge en une commande :
|
||||
@@ -1599,6 +1611,41 @@ Un claim **absent** échoue aussi (la dérive de formulation qui ferait
|
||||
disparaître un chiffre est elle-même une régression de traçabilité). stdlib pur
|
||||
(bash/git/python3), zéro réseau.
|
||||
|
||||
### `check_mobile_workflow.sh` — intégrité du second workflow `mobile-build.yml`
|
||||
`mobile-build.yml` (EAS build iOS/Android · Sprint 5) est **hors `gate.needs`** :
|
||||
aucun de ses jobs ne conditionne un merge (ils s'activent seulement quand Michel
|
||||
fournit `EAS_TOKEN`). Conséquence longtemps tolérée : **aucun gate ne lintait sa
|
||||
structure** — `check_ci_integrity.sh` ne verrouille que `ci.yml`, `validate_json.sh`
|
||||
que le `.json`. Mutation vérifiée : réintroduire `secrets.` dans un `if:` de job y
|
||||
passait **7/7 gates statiques verts**. Or ce fichier 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 ». Ce gate rend 4 invariants
|
||||
opposables, tous **lus dans le fichier** (jamais une liste à la main · #6) :
|
||||
|
||||
- **MOB-1 · bien-formé** — le fichier existe, porte `name:`/`on:`/`jobs:` et les
|
||||
3 jobs attendus (`preflight`, `build-ios`, `build-android`). Parse YAML strict
|
||||
**exigé si PyYAML est présent**, sinon contrôle awk seul (runner sans pip →
|
||||
best-effort ; jamais un faux vert, car MOB-2/3/4 ne dépendent pas de PyYAML).
|
||||
- **MOB-2 · portabilité du gating** — **0** `secrets.` dans un `if:` au niveau
|
||||
**job** (`^ if:`). Le contexte `secrets` n'est **pas** exposé dans
|
||||
`jobs.<id>.if` (table d'availability GitHub Actions) ; sur `act`/Gitea il
|
||||
s'évaluerait vide → job **SKIP silencieux** même token fourni. Exactement la
|
||||
régression corrigée en `234104` (passage au pattern preflight-output).
|
||||
- **MOB-3 · activation différée préservée (#6/#8)** — chaque job de **build**
|
||||
(bloc lançant `run: 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 `has_token`/`has_repo`,
|
||||
chaque `needs.preflight.outputs.<X>` référencé est bien **déclaré** (0 dangling →
|
||||
pas de SKIP par output fantôme) et chaque output déclaré est **alimenté** par un
|
||||
`echo "<X>=… >> $GITHUB_OUTPUT"`.
|
||||
|
||||
Les 5 mutations correspondantes (YAML cassé · `secrets.` en `if:` de job · `if:`
|
||||
de build supprimé · output fantôme référencé · output non alimenté) rendent toutes
|
||||
le gate **ROUGE** ; le fichier propre repasse **vert**. stdlib + awk pur, PyYAML
|
||||
best-effort, zéro réseau. Ce gate est le **8ᵉ gate statique** `ci/*.sh` (câblé dans
|
||||
`gate.needs`, donc rejoué par `run_ci.sh`).
|
||||
|
||||
### `lib.sh` — helper partagé (sourcé, jamais exécuté seul)
|
||||
Les gates commençaient tous par `cd "$(git rev-parse --show-toplevel)"`. **Hors**
|
||||
d'un arbre de travail git (tarball, `git archive | tar -x`, `git` absent du PATH),
|
||||
|
||||
Executable
+172
@@ -0,0 +1,172 @@
|
||||
#!/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.<id>.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.<X>` 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 "<X>=… >> $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é `^ <nom>:` à 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' ' ' </tmp/mobyaml)"
|
||||
fi
|
||||
else
|
||||
ok "structure awk OK · jobs = [$(printf '%s' "$JOBS" | paste -sd, -)] (PyYAML absent → parse strict sauté, invariants portants MOB-2/3/4 indépendants)"
|
||||
fi
|
||||
|
||||
# --- MOB-2 : portabilité du gating (aucun secrets. dans un if: de job) -------
|
||||
echo "== MOB-2 · aucun \`secrets.\` dans un \`if:\` au niveau job (portabilité act/Gitea) =="
|
||||
if [[ -n "$JOB_IF_SECRETS" ]]; then
|
||||
while IFS= read -r ln; do
|
||||
report "\`secrets.\` dans un if: de job (contexte indisponible → SKIP silencieux) : L${ln%%:*}"
|
||||
done <<< "$JOB_IF_SECRETS"
|
||||
else
|
||||
ok "0 référence \`secrets.\` en if: de job (le correctif preflight-output 234104 tient)"
|
||||
fi
|
||||
|
||||
# --- MOB-3 : gating d'activation préservé sur chaque job de build -----------
|
||||
echo "== MOB-3 · chaque job de build gaté sur needs.preflight.outputs.has_token =="
|
||||
if [[ -z "$BUILD_JOBS" ]]; then
|
||||
report "aucun job de build (\`eas build\`) détecté — le workflow ne builderait rien"
|
||||
fi
|
||||
for j in $BUILD_JOBS; do
|
||||
cond="$(job_if "$j")"
|
||||
if [[ -z "$cond" ]]; then
|
||||
report "job de build '$j' SANS if: job-level → builderait même sans EAS_TOKEN (#6/#8)"
|
||||
elif ! grep -q 'needs\.preflight\.outputs\.has_token' <<< "$cond"; then
|
||||
report "job de build '$j' non gaté sur needs.preflight.outputs.has_token → activation non contrôlée"
|
||||
else
|
||||
ok "job de build '$j' gaté sur needs.preflight.outputs.has_token (SKIP tant que token absent)"
|
||||
fi
|
||||
done
|
||||
|
||||
# --- MOB-4 : contrat d'outputs intact (déclarés · référencés · alimentés) ---
|
||||
echo "== MOB-4 · contrat outputs preflight (déclarés == référencés == alimentés) =="
|
||||
# Outputs DÉCLARÉS dans le bloc `outputs:` de preflight.
|
||||
DECLARED="$(awk '
|
||||
/^jobs:[[:space:]]*$/ { injobs=1; next }
|
||||
injobs && match($0, /^ ([A-Za-z0-9_-]+):[[:space:]]*$/, m) { job=m[1]; inout=0; next }
|
||||
injobs && job=="preflight" && /^ outputs:[[:space:]]*$/ { inout=1; next }
|
||||
injobs && job=="preflight" && inout && match($0, /^ ([A-Za-z0-9_]+):/, m) { print m[1]; next }
|
||||
injobs && job=="preflight" && inout && /^ [A-Za-z]/ { inout=0 }
|
||||
' "$WF" | sort -u)"
|
||||
|
||||
# Outputs RÉFÉRENCÉS via needs.preflight.outputs.<X> n'importe où.
|
||||
REFERENCED="$(grep -oE 'needs\.preflight\.outputs\.[A-Za-z0-9_]+' "$WF" | sed 's/.*outputs\.//' | sort -u)"
|
||||
# Outputs ALIMENTÉS via echo "<X>=… >> $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"
|
||||
Reference in New Issue
Block a user