veza/run-audit.sh
senke e148c52481 chore: add audit screenshots, audit scripts, and prompt templates
Visual audit captures for all major pages (desktop, tablet, mobile).
Add run-audit.sh and generate_page_fix_prompts.sh helper scripts.
Add prompt templates directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 19:17:05 +02:00

502 lines
17 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
#
# VEZA — Script d'audit automatique overnight
# Lance Claude Code sur chaque prompt d'audit, avec reprise sur erreur.
#
# Usage:
# ./run-audit.sh # Lance depuis le début (ou reprend)
# ./run-audit.sh --reset # Repart de zéro
# ./run-audit.sh --from 15 # Force le démarrage au prompt 15
# ./run-audit.sh --only 38 # N'exécute qu'un seul prompt
# ./run-audit.sh --dry-run # Affiche ce qui serait fait sans exécuter
# ./run-audit.sh --status # Affiche l'état actuel
# ./run-audit.sh --model sonnet # Utilise Sonnet au lieu d'Opus (moins cher)
#
# Coût estimé (Opus): ~$150400 pour les 58 prompts
# Coût estimé (Sonnet): ~$3080 pour les 58 prompts
# Durée estimée: 48 heures selon le modèle et la charge API
#
set -euo pipefail
# ─────────────────────────────────────────────
# Configuration
# ─────────────────────────────────────────────
REPO_ROOT="$(cd "$(dirname "$0")" && pwd)"
PROMPTS_DIR="$REPO_ROOT/prompts"
STATE_DIR="$REPO_ROOT/.audit-state"
STATE_FILE="$STATE_DIR/progress.json"
LOG_DIR="$STATE_DIR/logs"
BRANCH_NAME="audit/full-page-audit-$(date +%Y%m%d)"
# Claude CLI settings
MODEL="${VEZA_AUDIT_MODEL:-opus}"
MAX_BUDGET_PER_PROMPT="${VEZA_AUDIT_BUDGET_PER_PROMPT:-8}" # USD par prompt
MAX_RETRIES=3
RETRY_BACKOFF_BASE=60 # secondes, doublé à chaque retry
COOLDOWN_BETWEEN_PROMPTS=10 # secondes entre chaque prompt
RATE_LIMIT_PAUSE=300 # 5 min de pause si rate limit détecté
# Couleurs
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# ─────────────────────────────────────────────
# Fonctions utilitaires
# ─────────────────────────────────────────────
log() { echo -e "${CYAN}[$(date '+%H:%M:%S')]${NC} $*"; }
info() { echo -e "${GREEN}[$(date '+%H:%M:%S')] ✓${NC} $*"; }
warn() { echo -e "${YELLOW}[$(date '+%H:%M:%S')] ⚠${NC} $*"; }
error() { echo -e "${RED}[$(date '+%H:%M:%S')] ✗${NC} $*"; }
die() { error "$*"; exit 1; }
# ─────────────────────────────────────────────
# State management (fichier JSON simple via jq)
# ─────────────────────────────────────────────
ensure_deps() {
command -v claude >/dev/null 2>&1 || die "claude CLI non trouvé. Installe-le d'abord."
command -v jq >/dev/null 2>&1 || die "jq non trouvé. Installe: sudo dnf install jq"
command -v git >/dev/null 2>&1 || die "git non trouvé."
}
init_state() {
mkdir -p "$STATE_DIR" "$LOG_DIR"
if [[ ! -f "$STATE_FILE" ]]; then
cat > "$STATE_FILE" <<'ENDJSON'
{
"started_at": null,
"branch": null,
"prompts": {},
"current": null,
"total_cost_estimate_usd": 0,
"total_prompts": 0,
"completed": 0,
"failed": 0,
"skipped": 0
}
ENDJSON
log "State file initialisé: $STATE_FILE"
fi
}
state_get() {
jq -r "$1" "$STATE_FILE"
}
state_set() {
local tmp
tmp=$(mktemp)
jq "$1" "$STATE_FILE" > "$tmp" && mv "$tmp" "$STATE_FILE"
}
get_prompt_status() {
local num="$1"
jq -r ".prompts[\"$num\"].status // \"pending\"" "$STATE_FILE"
}
set_prompt_status() {
local num="$1" status="$2" detail="${3:-}"
state_set ".prompts[\"$num\"] = {
\"status\": \"$status\",
\"updated_at\": \"$(date -Iseconds)\",
\"detail\": \"$detail\"
}"
# Update counters
local completed failed
completed=$(jq '[.prompts[] | select(.status == "done")] | length' "$STATE_FILE")
failed=$(jq '[.prompts[] | select(.status == "failed")] | length' "$STATE_FILE")
state_set ".completed = $completed | .failed = $failed"
}
# ─────────────────────────────────────────────
# Git helpers
# ─────────────────────────────────────────────
setup_branch() {
cd "$REPO_ROOT"
local current_branch
current_branch=$(git branch --show-current)
local existing_branch
existing_branch=$(state_get '.branch // empty')
if [[ -n "$existing_branch" ]] && git rev-parse --verify "$existing_branch" >/dev/null 2>&1; then
# Reprendre sur la branche existante
if [[ "$current_branch" != "$existing_branch" ]]; then
log "Reprise sur branche existante: $existing_branch"
git checkout "$existing_branch"
fi
BRANCH_NAME="$existing_branch"
else
# Créer nouvelle branche depuis l'état actuel
if git rev-parse --verify "$BRANCH_NAME" >/dev/null 2>&1; then
BRANCH_NAME="${BRANCH_NAME}-$(date +%H%M%S)"
fi
log "Création branche: $BRANCH_NAME"
git checkout -b "$BRANCH_NAME"
state_set ".branch = \"$BRANCH_NAME\""
fi
state_set ".started_at = \"$(date -Iseconds)\""
}
commit_prompt_changes() {
local num="$1" name="$2" path="$3"
cd "$REPO_ROOT"
# Vérifier s'il y a des changements
if git diff --quiet && git diff --cached --quiet && [[ -z "$(git ls-files --others --exclude-standard)" ]]; then
warn "Prompt $num ($path): aucun changement à committer"
return 0
fi
# Stage tous les changements (sauf les fichiers d'état de l'audit)
git add -A -- ':!.audit-state'
# Vérifier à nouveau après staging
if git diff --cached --quiet; then
warn "Prompt $num ($path): rien dans le staging"
return 0
fi
git commit -m "$(cat <<EOF
audit($path): fix bugs and add e2e tests — $name
Automated audit via Claude Code + Playwright MCP.
- Full page audit: loading, features, a11y, i18n, responsive, security
- Bug fixes applied
- E2E regression tests added
Prompt: prompts/$(printf '%02d' "$num")-*.md
EOF
)"
info "Commit créé pour prompt $num ($path)"
}
# ─────────────────────────────────────────────
# Exécution d'un prompt
# ─────────────────────────────────────────────
run_single_prompt() {
local prompt_file="$1"
local num
num=$(basename "$prompt_file" | grep -oP '^\d+')
# Enlever le zéro devant pour le numéro
num=$((10#$num))
local log_file="$LOG_DIR/prompt-$(printf '%02d' "$num").log"
local route_name
route_name=$(basename "$prompt_file" .md | sed 's/^[0-9]*-//')
log "━━━ Prompt $num/58: $route_name ━━━"
local status
status=$(get_prompt_status "$num")
if [[ "$status" == "done" ]]; then
info "Déjà complété, skip"
return 0
fi
set_prompt_status "$num" "running"
state_set ".current = $num"
local prompt_content
prompt_content=$(cat "$prompt_file")
local attempt=0
local success=false
while (( attempt < MAX_RETRIES )); do
attempt=$((attempt + 1))
log "Tentative $attempt/$MAX_RETRIES..."
local exit_code=0
# Exécuter Claude Code
# --print: mode non-interactif
# --dangerously-skip-permissions: aucune interaction
# --max-budget-usd: garde-fou coût par prompt
timeout 1800 claude \
-p \
--dangerously-skip-permissions \
--model "$MODEL" \
--max-budget-usd "$MAX_BUDGET_PER_PROMPT" \
"$prompt_content" \
> "$log_file" 2>&1 \
|| exit_code=$?
if [[ $exit_code -eq 0 ]]; then
success=true
break
fi
# Analyser l'erreur
local last_lines
last_lines=$(tail -20 "$log_file" 2>/dev/null || echo "")
if echo "$last_lines" | grep -qi "rate.limit\|429\|too many requests\|overloaded"; then
warn "Rate limit détecté. Pause de ${RATE_LIMIT_PAUSE}s..."
sleep "$RATE_LIMIT_PAUSE"
elif echo "$last_lines" | grep -qi "context.window\|token.limit\|max.*token\|session.*limit"; then
warn "Limite de session/contexte atteinte. Prompt trop lourd, on continue."
break
elif echo "$last_lines" | grep -qi "network\|connection\|ECONNRESET\|ETIMEDOUT\|fetch failed"; then
local backoff=$((RETRY_BACKOFF_BASE * (2 ** (attempt - 1))))
warn "Erreur réseau. Backoff ${backoff}s..."
sleep "$backoff"
elif [[ $exit_code -eq 124 ]]; then
warn "Timeout (30min). Le prompt a pris trop de temps."
break
else
local backoff=$((RETRY_BACKOFF_BASE * (2 ** (attempt - 1))))
warn "Erreur inconnue (code $exit_code). Backoff ${backoff}s..."
sleep "$backoff"
fi
done
if $success; then
# Commit les changements
commit_prompt_changes "$num" "$route_name" "$(head -1 "$prompt_file" | grep -oP '/[a-z/:-]+')" || true
set_prompt_status "$num" "done"
info "Prompt $num terminé avec succès"
else
set_prompt_status "$num" "failed" "after $attempt attempts, exit code $exit_code"
error "Prompt $num échoué après $attempt tentatives"
# On continue quand même avec le suivant
fi
# Cooldown entre les prompts
if (( COOLDOWN_BETWEEN_PROMPTS > 0 )); then
log "Cooldown ${COOLDOWN_BETWEEN_PROMPTS}s..."
sleep "$COOLDOWN_BETWEEN_PROMPTS"
fi
}
# ─────────────────────────────────────────────
# Collecte des prompts
# ─────────────────────────────────────────────
get_prompt_files() {
find "$PROMPTS_DIR" -name '*.md' -not -name 'ALL-PROMPTS.md' | sort
}
# ─────────────────────────────────────────────
# Commandes principales
# ─────────────────────────────────────────────
cmd_status() {
if [[ ! -f "$STATE_FILE" ]]; then
log "Aucun audit en cours."
return
fi
echo ""
echo -e "${CYAN}═══ VEZA Audit Status ═══${NC}"
echo ""
echo -e " Branche: $(state_get '.branch // "non démarrée"')"
echo -e " Démarré: $(state_get '.started_at // "—"')"
echo -e " Complétés: ${GREEN}$(state_get '.completed')${NC}"
echo -e " Échoués: ${RED}$(state_get '.failed')${NC}"
echo -e " En cours: $(state_get '.current // "—"')"
echo ""
# Détail par prompt
local total=0 done=0 failed=0 pending=0
for f in $(get_prompt_files); do
total=$((total + 1))
local num
num=$((10#$(basename "$f" | grep -oP '^\d+')))
local st
st=$(get_prompt_status "$num")
local icon="⏳"
case "$st" in
done) icon="✅"; done=$((done + 1)) ;;
failed) icon="❌"; failed=$((failed + 1)) ;;
running) icon="🔄" ;;
*) pending=$((pending + 1)) ;;
esac
printf " %s %02d. %s\n" "$icon" "$num" "$(basename "$f" .md | sed 's/^[0-9]*-//')"
done
echo ""
echo -e " Total: $total | ✅ $done | ❌ $failed | ⏳ $pending"
echo ""
}
cmd_reset() {
warn "Reset de l'état d'audit..."
rm -rf "$STATE_DIR"
info "État supprimé. Relance ./run-audit.sh pour recommencer."
}
cmd_run() {
local start_from="${1:-0}"
local only="${2:-0}"
ensure_deps
init_state
local prompt_files
mapfile -t prompt_files < <(get_prompt_files)
local total=${#prompt_files[@]}
state_set ".total_prompts = $total"
log "═══════════════════════════════════════════════════════"
log " VEZA — Audit automatique de $total pages"
log " Modèle: $MODEL"
log " Budget max par prompt: \$${MAX_BUDGET_PER_PROMPT}"
log " Budget max total estimé: \$$(echo "$total * $MAX_BUDGET_PER_PROMPT" | bc)"
log "═══════════════════════════════════════════════════════"
setup_branch
for prompt_file in "${prompt_files[@]}"; do
local num
num=$((10#$(basename "$prompt_file" | grep -oP '^\d+')))
# --from: sauter les prompts avant
if (( start_from > 0 && num < start_from )); then
continue
fi
# --only: n'exécuter qu'un seul
if (( only > 0 && num != only )); then
continue
fi
run_single_prompt "$prompt_file"
done
state_set ".current = null"
# Résumé final
echo ""
log "═══════════════════════════════════════════════════════"
log " AUDIT TERMINÉ"
log "═══════════════════════════════════════════════════════"
cmd_status
local failed_count
failed_count=$(state_get '.failed')
if (( failed_count > 0 )); then
warn "$failed_count prompts ont échoué. Relance pour retenter uniquement les échecs."
warn "Les logs sont dans: $LOG_DIR/"
fi
info "Branche: $BRANCH_NAME"
info "Commits: $(git log --oneline "$BRANCH_NAME" --not main | wc -l) commits"
info "Logs: $LOG_DIR/"
}
cmd_dry_run() {
init_state
local prompt_files
mapfile -t prompt_files < <(get_prompt_files)
echo ""
log "═══ DRY RUN — Voici ce qui serait exécuté ═══"
echo ""
for prompt_file in "${prompt_files[@]}"; do
local num
num=$((10#$(basename "$prompt_file" | grep -oP '^\d+')))
local st
st=$(get_prompt_status "$num")
local action="EXÉCUTER"
[[ "$st" == "done" ]] && action="SKIP (déjà fait)"
printf " %02d. %-40s → %s\n" "$num" "$(basename "$prompt_file")" "$action"
done
local pending
pending=$(jq '[.prompts[] | select(.status == "done")] | length' "$STATE_FILE" 2>/dev/null || echo 0)
local total=${#prompt_files[@]}
local to_run=$((total - pending))
echo ""
echo " À exécuter: $to_run / $total prompts"
echo " Modèle: $MODEL"
echo " Budget max: \$$(echo "$to_run * $MAX_BUDGET_PER_PROMPT" | bc)"
echo " Durée estimée: $(echo "$to_run * 8" | bc) min (≈$(echo "$to_run * 8 / 60" | bc)h)"
echo ""
}
# ─────────────────────────────────────────────
# CLI parsing
# ─────────────────────────────────────────────
main() {
cd "$REPO_ROOT"
local start_from=0
local only=0
local dry_run=false
while [[ $# -gt 0 ]]; do
case "$1" in
--reset)
cmd_reset
exit 0
;;
--status)
cmd_status
exit 0
;;
--dry-run)
dry_run=true
shift
;;
--from)
start_from="$2"
shift 2
;;
--only)
only="$2"
shift 2
;;
--model)
MODEL="$2"
shift 2
;;
--budget)
MAX_BUDGET_PER_PROMPT="$2"
shift 2
;;
-h|--help)
echo "Usage: $0 [options]"
echo ""
echo "Options:"
echo " --reset Supprime l'état et repart de zéro"
echo " --status Affiche la progression actuelle"
echo " --dry-run Montre ce qui serait fait sans exécuter"
echo " --from N Commence au prompt N"
echo " --only N N'exécute que le prompt N"
echo " --model MODEL Modèle Claude (opus|sonnet|haiku) [défaut: opus]"
echo " --budget N Budget max USD par prompt [défaut: 8]"
echo ""
echo "Variables d'environnement:"
echo " VEZA_AUDIT_MODEL Modèle par défaut"
echo " VEZA_AUDIT_BUDGET_PER_PROMPT Budget par prompt"
exit 0
;;
*)
die "Option inconnue: $1 (utilise --help)"
;;
esac
done
if $dry_run; then
cmd_dry_run
else
cmd_run "$start_from" "$only"
fi
}
main "$@"