[DTP-Worker] Sprint 8 · buffer · Gate reproductibilité artefacts (ci/check_artifacts.sh) + fix dérive run_sheet demo 18→21
Nouveau gate CI bloquant : régénère chaque 05_deliverables_mvp/**/out/ depuis son générateur `build` et exige l'égalité byte-for-byte avec le fichier commité (découverte auto · stdlib pur · zéro pip). Attrape la dérive silencieuse d'un artefact qui auto-résout une valeur depuis un autre module et devient périmé quand la source grandit — dérive qu'aucune suite tests/ (fonctions, pas fichier commité) n'attrapait. Défaut détecté et corrigé par ce gate : demo/scenarios/out/run_sheet.json embarquait ci_modules_count=18 (buildé Sprint 7) alors que qa/audit_4big en compte désormais 21. Régénéré (diff 1 ligne). Consistance live re-vérifiée. Wiring : job check-artifacts + needs du gate agrégé (.gitea/workflows/ci.yml) · doc (ci/README.md). Vérifs : test négatif du gate OK (dérive → EXIT 1), 4 gates statiques EXIT 0, suites CI-parsing vertes (regression/acceptance/audit_4big/demo), 21/21 suites · 534 tests, compteurs dérivés stables. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+18
-1
@@ -43,6 +43,23 @@ jobs:
|
|||||||
- name: Contrôle docs
|
- name: Contrôle docs
|
||||||
run: bash ci/check_docs.sh
|
run: bash ci/check_docs.sh
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Reproductibilité des artefacts (Sprint 8 · DevOps CI/CD). Prouve que chaque
|
||||||
|
# `05_deliverables_mvp/**/out/*.json` versionné se régénère À L'IDENTIQUE
|
||||||
|
# depuis son générateur `build`. Attrape la dérive silencieuse d'un artefact
|
||||||
|
# qui auto-résout une valeur depuis un AUTRE module (ex. demo/scenarios lit
|
||||||
|
# qa/audit_4big/coverage) et devient périmé quand la source grandit — dérive
|
||||||
|
# qu'aucune suite `tests/` (qui teste des fonctions, pas le fichier commité)
|
||||||
|
# n'attrape. stdlib pur, zéro pip.
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
check-artifacts:
|
||||||
|
name: Reproductibilité des artefacts out/ (build == commité)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Contrôle reproductibilité artefacts
|
||||||
|
run: bash ci/check_artifacts.sh
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# Publiciste (Sprint 2) : parser faisabilité → projets_master.json + generator.
|
# Publiciste (Sprint 2) : parser faisabilité → projets_master.json + generator.
|
||||||
# Tests unitaires stdlib pur (unittest) → aucune installation pip requise sur
|
# Tests unitaires stdlib pur (unittest) → aucune installation pip requise sur
|
||||||
@@ -428,7 +445,7 @@ jobs:
|
|||||||
gate:
|
gate:
|
||||||
name: Gate qualité (agrégat)
|
name: Gate qualité (agrégat)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [constraints-guard, validate-json, check-docs, 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, 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]
|
needs: [constraints-guard, validate-json, check-docs, check-artifacts, 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, 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]
|
||||||
steps:
|
steps:
|
||||||
- name: Résultat
|
- name: Résultat
|
||||||
run: echo "✅ Gate qualité 4Big franchi — tous les checks verts."
|
run: echo "✅ Gate qualité 4Big franchi — tous les checks verts."
|
||||||
|
|||||||
@@ -1604,3 +1604,59 @@ rigueur de la spec**, explicitement **pas** un score d'implémentation (le build
|
|||||||
**Hors périmètre worker (VPS · #8)** : néant (changements in-repo : gate CI locale + doc).
|
**Hors périmètre worker (VPS · #8)** : néant (changements in-repo : gate CI locale + doc).
|
||||||
|
|
||||||
**Auto-score 4Big** : 96/100.
|
**Auto-score 4Big** : 96/100.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session 20260730_232914 · Buffer S8 · Gate reproductibilité des artefacts + fix dérive run_sheet demo (18→21)
|
||||||
|
|
||||||
|
**Tâche** : **Sprint 8 · « Regression tests exhaustifs » + DevOps CI/CD**. Baseline
|
||||||
|
sain confirmé au démarrage (4 gates statiques EXIT 0 ; 21/21 suites vertes · 534
|
||||||
|
tests via `qa/regression run`). Objectif : durcir la régression au-delà de
|
||||||
|
l'exécution des tests — prouver la **reproductibilité byte-for-byte** des artefacts
|
||||||
|
`out/` versionnés (aucun `out/*.json` n'a dérivé de sa source).
|
||||||
|
|
||||||
|
**Découverte d'un VRAI défaut (analyser avant agir · #1)** : en ré-exécutant le
|
||||||
|
`build` de chaque module vers un tmpdir et en diffant contre le commité, **1
|
||||||
|
artefact sur 18 dérivait** — `demo/scenarios/out/run_sheet.json` : `"valeur": 18`
|
||||||
|
là où le build frais produit `21`. Générateur **déterministe** (deux builds
|
||||||
|
identiques), donc pas de non-déterminisme : le fichier commité était **périmé**.
|
||||||
|
|
||||||
|
**Cause racine** : le run_sheet auto-résout ses valeurs de preuve depuis les
|
||||||
|
artefacts d'AUTRES modules — ici `qa/audit_4big` `/coverage/ci_modules_count`. Ce
|
||||||
|
compteur a grandi **18 → 21** (suites `regression`/`acceptance`/`deploy_runbook`
|
||||||
|
ajoutées depuis) mais le run_sheet — buildé au Sprint 7 (commit `cac6538`, 10:39)
|
||||||
|
— n'a jamais été régénéré. **Dérive silencieuse** qu'aucune suite `tests/` (qui
|
||||||
|
teste des FONCTIONS, pas le fichier commité) n'attrapait.
|
||||||
|
|
||||||
|
**2 solutions (workflow #4)** : A régénérer + gate systémique préventif ·
|
||||||
|
B régénérer seul. **Choix A** — B laisserait la dérive revenir au prochain ajout
|
||||||
|
de module. Correctif ponctuel : `demo_scenario_gen.py build -o out` (diff = 1
|
||||||
|
ligne, `18→21`) ; suite demo 32 tests OK ; consistance live re-vérifiée
|
||||||
|
(run_sheet `21` == audit_4big source `21`).
|
||||||
|
|
||||||
|
**Fichiers (4)** :
|
||||||
|
1. `ci/check_artifacts.sh` (**neuf**) — gate de reproductibilité. Découvre chaque
|
||||||
|
`05_deliverables_mvp/**/out/` (zéro liste à la main · #6), ré-exécute le
|
||||||
|
générateur `build -o <tmp>`, exige l'égalité byte-for-byte avec le commité.
|
||||||
|
Artefacts d'exécution non produits par `build` (p.ex.
|
||||||
|
`qa/regression/out/regression_run.json`) non comparés. stdlib pur (bash+python3),
|
||||||
|
zéro pip (runner Gitea · #2).
|
||||||
|
2. `.gitea/workflows/ci.yml` — job bloquant `check-artifacts` + ajout au `needs`
|
||||||
|
du `gate` agrégé.
|
||||||
|
3. `05_deliverables_mvp/demo/scenarios/out/run_sheet.json` — régénéré (`18→21`).
|
||||||
|
4. `ci/README.md` — table pipeline + section détail + bloc exécution locale.
|
||||||
|
|
||||||
|
**Anti-invention (#6)** : la valeur `21` n'est **pas saisie** — elle re-découle de
|
||||||
|
`qa/audit_4big/out/quality_report.json` (recalculé lui-même depuis le CI). Le gate
|
||||||
|
n'affirme QUE ce que le `build` déterministe produit.
|
||||||
|
|
||||||
|
**Vérifs** : **test négatif** du gate (dérive `21→18` réinjectée → EXIT 1 + message
|
||||||
|
« DÉRIVE ») puis test positif (EXIT 0, 18/18 modules reproductibles). 4 gates
|
||||||
|
statiques EXIT 0 · YAML `safe_load` OK · suites CI-parsing vertes
|
||||||
|
(regression 24 · acceptance 31 · audit_4big 34 · demo 32). `check-artifacts`
|
||||||
|
n'entre PAS comme « module » dans `parse_ci` (pas de working-directory) → compteurs
|
||||||
|
dérivés stables (21/534 inchangés).
|
||||||
|
|
||||||
|
**Hors périmètre worker (VPS · #8)** : néant (gate CI local + doc + artefact in-repo).
|
||||||
|
|
||||||
|
**Auto-score 4Big** : 96/100.
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Rapport quotidien · 2026-07-30 · Session 27 (20260730_232914)
|
||||||
|
|
||||||
|
## Sprint 8 · Buffer · Regression exhaustive + DevOps CI/CD
|
||||||
|
|
||||||
|
### Livrable
|
||||||
|
Nouveau **gate de reproductibilité des artefacts** (`ci/check_artifacts.sh`) +
|
||||||
|
correction d'une **dérive réelle** détectée par ce gate.
|
||||||
|
|
||||||
|
### Défaut corrigé (détecté, pas inventé)
|
||||||
|
`demo/scenarios/out/run_sheet.json` était **périmé** : il embarquait
|
||||||
|
`ci_modules_count = 18` alors que la source vivante (`qa/audit_4big`) en compte
|
||||||
|
désormais **21** (suites ajoutées depuis le Sprint 7). Générateur déterministe →
|
||||||
|
fichier commité jamais régénéré. Dérive **silencieuse** : les suites `tests/`
|
||||||
|
testent des fonctions, pas le fichier commité. Corrigé par régénération
|
||||||
|
(`build -o out`, diff 1 ligne `18→21`).
|
||||||
|
|
||||||
|
### Gate systémique (préventif)
|
||||||
|
`ci/check_artifacts.sh` régénère chaque `05_deliverables_mvp/**/out/` depuis son
|
||||||
|
générateur et exige l'**égalité byte-for-byte** avec le commité. Découverte auto
|
||||||
|
(zéro liste à la main). Bloquant en CI (`.gitea/workflows/ci.yml` → `check-artifacts`
|
||||||
|
dans `needs` du `gate`). stdlib pur, zéro pip.
|
||||||
|
|
||||||
|
### Vérifications
|
||||||
|
| Contrôle | Résultat |
|
||||||
|
|---|---|
|
||||||
|
| Test négatif du gate (dérive réinjectée) | ✅ EXIT 1 + « DÉRIVE » |
|
||||||
|
| Gate en état sain | ✅ 18/18 modules reproductibles · EXIT 0 |
|
||||||
|
| 4 gates statiques (guard/json/docs/artifacts) | ✅ EXIT 0 |
|
||||||
|
| Suites CI-parsing (regression/acceptance/audit_4big/demo) | ✅ 24 · 31 · 34 · 32 |
|
||||||
|
| Régression exhaustive | ✅ 21/21 suites · 534 tests |
|
||||||
|
| Compteurs dérivés stables | ✅ 21 / 534 inchangés |
|
||||||
|
|
||||||
|
### Hors périmètre worker (VPS · #8)
|
||||||
|
Néant — modifications 100 % in-repo (gate CI local, doc, artefact régénéré).
|
||||||
|
|
||||||
|
**Auto-score 4Big : 96/100**
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
"file": "out/quality_report.json",
|
"file": "out/quality_report.json",
|
||||||
"pointer": "/coverage/ci_modules_count",
|
"pointer": "/coverage/ci_modules_count",
|
||||||
"label": "Modules sous gate qualité",
|
"label": "Modules sous gate qualité",
|
||||||
"valeur": 18,
|
"valeur": 21,
|
||||||
"resolu": true
|
"resolu": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+17
-1
@@ -10,13 +10,14 @@
|
|||||||
## 1. Ce que fait le pipeline
|
## 1. Ce que fait le pipeline
|
||||||
|
|
||||||
Workflow : `.gitea/workflows/ci.yml`. Déclenché sur `push` / `pull_request` vers `main`
|
Workflow : `.gitea/workflows/ci.yml`. Déclenché sur `push` / `pull_request` vers `main`
|
||||||
et manuellement (`workflow_dispatch`). Quatre jobs :
|
et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` par module) :
|
||||||
|
|
||||||
| Job | Script | Rôle | Blocant |
|
| Job | Script | Rôle | Blocant |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `constraints-guard` | `ci/guard_constraints.sh` | Enforce les contraintes NON-NÉGOCIABLES de CLAUDE.md | ✅ oui |
|
| `constraints-guard` | `ci/guard_constraints.sh` | Enforce les contraintes NON-NÉGOCIABLES de CLAUDE.md | ✅ oui |
|
||||||
| `validate-json` | `ci/validate_json.sh` | Parse strict de tous les `*.json` (schémas Faisabilité↔Publiciste) | ✅ oui |
|
| `validate-json` | `ci/validate_json.sh` | Parse strict de tous les `*.json` (schémas Faisabilité↔Publiciste) | ✅ oui |
|
||||||
| `check-docs` | `ci/check_docs.sh` | Liens Markdown internes + présence auto-score 4Big | ✅ oui (liens) |
|
| `check-docs` | `ci/check_docs.sh` | Liens Markdown internes + présence auto-score 4Big | ✅ oui (liens) |
|
||||||
|
| `check-artifacts` | `ci/check_artifacts.sh` | Reproductibilité : chaque `out/*.json` versionné == build frais | ✅ oui |
|
||||||
| `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui |
|
| `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui |
|
||||||
|
|
||||||
Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec
|
Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec
|
||||||
@@ -47,12 +48,27 @@ Un JSON cassé casse le pipeline aval → attrapé ici.
|
|||||||
- **[SOFT]** livrables `05_deliverables_mvp/*.md` : mention d'auto-score 4Big
|
- **[SOFT]** livrables `05_deliverables_mvp/*.md` : mention d'auto-score 4Big
|
||||||
attendue (≥95/100, CLAUDE.md #5). Avertissement seul, non blocant.
|
attendue (≥95/100, CLAUDE.md #5). Avertissement seul, non blocant.
|
||||||
|
|
||||||
|
### `check_artifacts.sh`
|
||||||
|
Régénère chaque artefact `05_deliverables_mvp/**/out/` versionné depuis son
|
||||||
|
générateur (`build -o <tmp>`) et exige une **égalité byte-for-byte** avec le
|
||||||
|
fichier commité. Découverte automatique (zéro liste à la main · #6) : tout module
|
||||||
|
avec un `out/` et un générateur `build` entre dans le gate.
|
||||||
|
|
||||||
|
Cible la **dérive silencieuse** : un module auto-résout des valeurs depuis les
|
||||||
|
artefacts d'**autres** modules (ex. `demo/scenarios` lit
|
||||||
|
`qa/audit_4big/coverage/ci_modules_count`) ; quand la source grandit, l'artefact
|
||||||
|
consommateur devient périmé s'il n'est pas régénéré — dérive qu'aucune suite
|
||||||
|
`tests/` (qui teste des fonctions, pas le fichier commité) n'attrape. Correctif :
|
||||||
|
`build -o out` puis commit. Les artefacts d'**exécution** non produits par `build`
|
||||||
|
(p.ex. `qa/regression/out/regression_run.json`, issu de `run`) ne sont pas comparés.
|
||||||
|
|
||||||
## 3. Exécution locale (avant push)
|
## 3. Exécution locale (avant push)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash ci/guard_constraints.sh # contraintes CLAUDE.md
|
bash ci/guard_constraints.sh # contraintes CLAUDE.md
|
||||||
bash ci/validate_json.sh # schémas JSON
|
bash ci/validate_json.sh # schémas JSON
|
||||||
bash ci/check_docs.sh # liens + score
|
bash ci/check_docs.sh # liens + score
|
||||||
|
bash ci/check_artifacts.sh # reproductibilité out/
|
||||||
```
|
```
|
||||||
|
|
||||||
Chaque script retourne `0` si conforme, `1` sinon. Reproduit exactement ce que
|
Chaque script retourne `0` si conforme, `1` sinon. Reproduit exactement ce que
|
||||||
|
|||||||
Executable
+95
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# check_artifacts.sh · OTO Enterprise OS DTP
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Gate de REPRODUCTIBILITÉ des artefacts (roadmap Sprint 8 · « Regression tests
|
||||||
|
# exhaustifs » · DevOps CI/CD). Prouve, en une commande, que CHAQUE artefact
|
||||||
|
# `out/` versionné se régénère À L'IDENTIQUE (byte-for-byte) depuis son
|
||||||
|
# générateur — donc qu'aucun `out/*.json` n'a dérivé de sa source.
|
||||||
|
#
|
||||||
|
# Pourquoi ce gate : plusieurs modules auto-résolvent des valeurs depuis les
|
||||||
|
# artefacts d'AUTRES modules (ex. demo/scenarios lit qa/audit_4big). Quand la
|
||||||
|
# source grandit (un compteur passe de 18 à 21…), l'artefact consommateur
|
||||||
|
# devient périmé s'il n'est pas régénéré — dérive silencieuse qu'aucune suite
|
||||||
|
# `tests/` (qui teste des FONCTIONS, pas le fichier commité) n'attrape. Ce gate
|
||||||
|
# la bloque : il compare le build frais au fichier commité.
|
||||||
|
#
|
||||||
|
# Contrat vérifié : pour chaque `05_deliverables_mvp/**/out/`, le générateur
|
||||||
|
# racine (le .py portant une sous-commande `build`) est ré-exécuté vers un
|
||||||
|
# répertoire temporaire ; tout fichier produit DOIT exister et être identique
|
||||||
|
# à son jumeau commité. Les artefacts d'EXÉCUTION (non produits par `build`,
|
||||||
|
# p.ex. qa/regression/out/regression_run.json issu de `run`) ne sont pas
|
||||||
|
# comparés — le gate n'assert que ce que `build` (déterministe) produit.
|
||||||
|
#
|
||||||
|
# Dépendances : bash, git, python3, diff, mktemp. Aucun réseau requis
|
||||||
|
# (stdlib pur · CLAUDE.md #2 — runner Gitea sans pip).
|
||||||
|
# ============================================================================
|
||||||
|
set -uo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
FAIL=0
|
||||||
|
report() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; }
|
||||||
|
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
|
||||||
|
|
||||||
|
ROOT="05_deliverables_mvp"
|
||||||
|
|
||||||
|
# Découverte : tout répertoire `out/` sous les livrables. Zéro liste à la main
|
||||||
|
# (#6) — un module ajouté entre automatiquement dans le gate.
|
||||||
|
mapfile -t outdirs < <(find "$ROOT" -type d -name out | sort)
|
||||||
|
|
||||||
|
if [[ "${#outdirs[@]}" -eq 0 ]]; then
|
||||||
|
echo "Aucun répertoire out/ — rien à vérifier."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== Reproductibilité des artefacts out/ (build frais == commité ?) =="
|
||||||
|
for outdir in "${outdirs[@]}"; do
|
||||||
|
mod="$(dirname "$outdir")"
|
||||||
|
rel="${mod#"$ROOT"/}"
|
||||||
|
|
||||||
|
# Générateur = l'unique .py à la racine du module portant `add_parser("build")`.
|
||||||
|
gen="$(grep -rln 'add_parser("build"\|add_parser('\''build'\''' "$mod"/*.py 2>/dev/null | head -1)"
|
||||||
|
if [[ -z "$gen" ]]; then
|
||||||
|
report "$rel — répertoire out/ sans générateur 'build' (config invalide)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
if ! ( cd "$mod" && python3 "$(basename "$gen")" build -o "$tmp" ) >/dev/null 2>"$tmp/.err"; then
|
||||||
|
report "$rel — 'build' a échoué :"
|
||||||
|
sed 's/^/ /' "$tmp/.err"
|
||||||
|
rm -rf "$tmp"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
drift=0
|
||||||
|
produced=0
|
||||||
|
for f in "$tmp"/*; do
|
||||||
|
[[ -e "$f" ]] || continue
|
||||||
|
bn="$(basename "$f")"
|
||||||
|
produced=$((produced + 1))
|
||||||
|
committed="$outdir/$bn"
|
||||||
|
if [[ ! -f "$committed" ]]; then
|
||||||
|
report "$rel/$bn — produit par 'build' mais ABSENT du dépôt (non commité)"
|
||||||
|
drift=1
|
||||||
|
elif ! diff -q "$f" "$committed" >/dev/null 2>&1; then
|
||||||
|
report "$rel/$bn — DÉRIVE : le fichier commité diffère du build frais (régénérer)"
|
||||||
|
drift=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$produced" -eq 0 ]]; then
|
||||||
|
report "$rel — 'build' n'a produit aucun fichier (générateur cassé ?)"
|
||||||
|
elif [[ "$drift" -eq 0 ]]; then
|
||||||
|
ok "$rel — $produced artefact(s) reproductible(s) à l'identique"
|
||||||
|
fi
|
||||||
|
rm -rf "$tmp"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [[ "$FAIL" -eq 0 ]]; then
|
||||||
|
echo -e "\033[32m✅ Tous les artefacts out/ sont reproductibles (aucune dérive).\033[0m"
|
||||||
|
else
|
||||||
|
echo -e "\033[31m❌ Artefact périmé/dérivé détecté — régénérer avec 'build -o out'.\033[0m"
|
||||||
|
fi
|
||||||
|
exit "$FAIL"
|
||||||
Reference in New Issue
Block a user