Apunta los scripts de sync prod al Hetzner nuevo y suma trabajo pendiente
- sync_translations_to_prod.py / sync_audio_to_prod.py / sync_carta_from_prod.py: migran de FEA_PROD_HOST/PASS (CDMON, password) a FEA_PROD_SSH_HOST/PASS + FEA_PROD_DOCKER_CONTAINER (Hetzner/Coolify, auth por clave), con wrapping docker exec y el fix del bug de redirecciones (wc -c/cat) resuelto en el host en vez de dentro del contenedor. - fea_translate_helper.php: subcomando clone_new para clonar en ID local nuevo cuando el ID de prod ya está ocupado localmente. - translate_post.py: --dry-run. - tts_produce.py: --allow-default-voice (voz Nico solo si se permite explícitamente para autores sin voz clonada) + fix voice_for_author. - minimax_tts.py: parámetro speed en t2a/_synth_chunk. - mirror-antiguo/deploy/nginx-mirror.conf: redirects de URLs históricas de catálogo y vista imprimible EFFA. - Importaciones humanas de Pagola (carta 738) + scripts/manifest de la fase A Enrique, y release_issue181_prod.sh / cdmon-retire-fewp1.sh. - .gitignore: excluye docs/backups/ (dumps SQL, mismo motivo que backups/*). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+167
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env bash
|
||||
# Release Fase 1+2, issue #181: 68 traducciones publicadas + 16 MP3 TTS.
|
||||
# Por defecto es sólo dry-run. --apply requiere confirmación explícita de Rafa.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
MODE="dry-run"
|
||||
if [[ "${1:-}" == "--apply" ]]; then
|
||||
MODE="apply"
|
||||
elif [[ "${1:-}" == "--verify" ]]; then
|
||||
MODE="verify"
|
||||
elif [[ "${1:-}" != "" && "${1:-}" != "--dry-run" ]]; then
|
||||
echo "Uso: $0 [--dry-run|--verify|--apply]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source ~/.hermes/profiles/feadulta/.env
|
||||
: "${FEA_PROD_WPLOAD:=/web/wp-load.php}"
|
||||
if [[ "$FEA_PROD_WPLOAD" != "/web/wp-load.php" ]]; then
|
||||
echo "ABORT: FEA_PROD_WPLOAD debe ser /web/wp-load.php (recibido: $FEA_PROD_WPLOAD)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
RELEASE_DIR="logs/release-issue-181"
|
||||
mkdir -p "$RELEASE_DIR/backups"
|
||||
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
ORIGINS=(54875 54902 54903 54883 54884 54885 54886 54866 54867 54868 54869 54870 54871 54872 54873 54880 54914)
|
||||
AUDIO_IDS=(54875 54902 54903 54883 54884 54885 54886 54866 54867 54868 54869 54870 54871 54872 54873 54880)
|
||||
|
||||
if [[ "$MODE" == "apply" && "${FEA_RELEASE_CONFIRM:-}" != "PUBLISH_ISSUE_181" ]]; then
|
||||
echo "ABORT: para escribir en producción usa:" >&2
|
||||
echo " FEA_RELEASE_CONFIRM=PUBLISH_ISSUE_181 $0 --apply" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
preflight() {
|
||||
python3 - "${ORIGINS[@]}" <<'PY'
|
||||
import json, sys
|
||||
sys.path.insert(0, 'scripts')
|
||||
import sync_translations_to_prod as sync
|
||||
ids = [int(x) for x in sys.argv[1:]]
|
||||
rows = []
|
||||
for pid in ids:
|
||||
d = json.loads(sync.prod_helper('read_full', str(pid)))
|
||||
rows.append({'id': pid, 'status': d['status'], 'lang': d['lang'], 'translations': d.get('translations', {})})
|
||||
assert len(rows) == 17
|
||||
assert all(r['status'] == 'publish' and r['lang'] == 'es' for r in rows), rows
|
||||
# Los 16 artículos no deben tener traducciones aún; la carta tampoco antes del primer release.
|
||||
assert all(r['translations'] == {'es': r['id']} for r in rows), rows
|
||||
print(json.dumps(rows, ensure_ascii=False, indent=2))
|
||||
print('preflight=PASS sources=17')
|
||||
PY
|
||||
}
|
||||
|
||||
backup_prod_state() {
|
||||
python3 - "$RELEASE_DIR/backups/prod-before-$STAMP.json" "${ORIGINS[@]}" -- "${AUDIO_IDS[@]}" <<'PY'
|
||||
import json, sys
|
||||
out = sys.argv[1]
|
||||
sep = sys.argv.index('--')
|
||||
origins = [int(x) for x in sys.argv[2:sep]]
|
||||
audio_ids = [int(x) for x in sys.argv[sep+1:]]
|
||||
sys.path.insert(0, 'scripts')
|
||||
import sync_translations_to_prod as translations
|
||||
import sync_audio_to_prod as audio
|
||||
payload = {
|
||||
'origins': {str(pid): json.loads(translations.prod_helper('read_full', str(pid))) for pid in origins},
|
||||
'audio_before': {
|
||||
str(pid): {
|
||||
'url': audio.prod_helper('getmeta', str(pid), 'fea_audio_url').strip(),
|
||||
'voice': audio.prod_helper('getmeta', str(pid), 'fea_audio_voice').strip(),
|
||||
'done': audio.prod_helper('getmeta', str(pid), 'fea_audio_done').strip(),
|
||||
} for pid in audio_ids
|
||||
},
|
||||
}
|
||||
with open(out, 'w', encoding='utf-8') as fh:
|
||||
json.dump(payload, fh, ensure_ascii=False, indent=2)
|
||||
print(out)
|
||||
PY
|
||||
}
|
||||
|
||||
verify_release() {
|
||||
python3 - "${ORIGINS[@]}" -- "${AUDIO_IDS[@]}" <<'PY'
|
||||
import json, sys, time
|
||||
sep = sys.argv.index('--')
|
||||
origins = [int(x) for x in sys.argv[1:sep]]
|
||||
audio_ids = [int(x) for x in sys.argv[sep+1:]]
|
||||
sys.path.insert(0, 'scripts')
|
||||
import sync_translations_to_prod as translations
|
||||
import sync_audio_to_prod as audio
|
||||
|
||||
def read_post(pid):
|
||||
"""Lectura server-side resiliente ante una respuesta SSH/PHP vacía transitoria."""
|
||||
last = ''
|
||||
for attempt in range(1, 4):
|
||||
raw = translations.prod_helper('read_full', str(pid)).strip()
|
||||
if raw:
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
last = f'JSON inválido intento {attempt}: {exc}; prefijo={raw[:160]!r}'
|
||||
else:
|
||||
last = f'respuesta vacía intento {attempt}'
|
||||
time.sleep(attempt)
|
||||
raise AssertionError(f'No se pudo leer post #{pid}: {last}')
|
||||
|
||||
for pid in origins:
|
||||
d = read_post(pid)
|
||||
group = d.get('translations', {})
|
||||
assert set(group) == {'es', 'en', 'fr', 'it', 'pt'}, (pid, group)
|
||||
for lang, tid in group.items():
|
||||
td = read_post(tid)
|
||||
assert td['lang'] == lang and td['status'] == 'publish', (pid, lang, tid, td['lang'], td['status'])
|
||||
for pid in audio_ids:
|
||||
url = audio.prod_helper('getmeta', str(pid), 'fea_audio_url').strip()
|
||||
done = audio.prod_helper('getmeta', str(pid), 'fea_audio_done').strip()
|
||||
assert url.endswith(f'/wp-content/uploads/tts/{pid}.mp3') and done == '1', (pid, url, done)
|
||||
print('verification=PASS groups=17 translations=68 audio=16')
|
||||
PY
|
||||
}
|
||||
|
||||
if [[ "$MODE" == "verify" ]]; then
|
||||
echo "== Verificación server-side posterior (solo lectura) =="
|
||||
verify_release | tee "$RELEASE_DIR/verification-$STAMP.txt"
|
||||
echo "VERIFICACIÓN ISSUE #181 COMPLETADA"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "== Preflight server-side ($MODE): 17 fuentes ES =="
|
||||
preflight | tee "$RELEASE_DIR/preflight-$MODE-$STAMP.json"
|
||||
|
||||
if [[ "$MODE" == "dry-run" ]]; then
|
||||
: > "$RELEASE_DIR/translations-dry-run-$STAMP.log"
|
||||
for origin in "${ORIGINS[@]}"; do
|
||||
FEA_SYNC_STATUS=publish \
|
||||
FEA_SYNC_LOG="$RELEASE_DIR/translations-dry-run-$STAMP.log" \
|
||||
FEA_SYNC_STATE="$RELEASE_DIR/translations-$origin-state.json" \
|
||||
python3 scripts/sync_translations_to_prod.py --origin "$origin" --dry-run
|
||||
done
|
||||
FEA_AUDIO_SYNC_LOG="$RELEASE_DIR/audio-dry-run-$STAMP.log" \
|
||||
FEA_AUDIO_SYNC_STATE="$RELEASE_DIR/audio-state.json" \
|
||||
python3 scripts/sync_audio_to_prod.py --ids "$(IFS=,; echo "${AUDIO_IDS[*]}")" --dry-run | tee "$RELEASE_DIR/audio-dry-run-$STAMP.log"
|
||||
echo "DRY-RUN terminado: 68 traducciones publicables + 16 audios planificados."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "== Backup server-side previo =="
|
||||
backup_prod_state | tee "$RELEASE_DIR/backup-path-$STAMP.txt"
|
||||
|
||||
echo "== Subiendo 68 traducciones como publish =="
|
||||
for origin in "${ORIGINS[@]}"; do
|
||||
FEA_SYNC_STATUS=publish \
|
||||
FEA_SYNC_LOG="$RELEASE_DIR/translations-apply-$STAMP.log" \
|
||||
FEA_SYNC_STATE="$RELEASE_DIR/translations-$origin-state.json" \
|
||||
python3 scripts/sync_translations_to_prod.py --origin "$origin"
|
||||
done
|
||||
|
||||
echo "== Subiendo 16 audios =="
|
||||
FEA_AUDIO_SYNC_LOG="$RELEASE_DIR/audio-apply-$STAMP.log" \
|
||||
FEA_AUDIO_SYNC_STATE="$RELEASE_DIR/audio-state.json" \
|
||||
python3 scripts/sync_audio_to_prod.py --ids "$(IFS=,; echo "${AUDIO_IDS[*]}")"
|
||||
|
||||
echo "== Verificación server-side posterior =="
|
||||
verify_release | tee "$RELEASE_DIR/verification-$STAMP.txt"
|
||||
echo "RELEASE ISSUE #181 COMPLETADO"
|
||||
Reference in New Issue
Block a user