Volver al índice
docs/publicar-raps.py
"""Compilación y publicación repetible de Raps; nunca publica una etapa fallida."""
from __future__ import annotations
import argparse
import hashlib
import inspect
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import time
import zipfile
ROOT = Path(__file__).resolve().parents[1]
WORK = ROOT / 'build/publicacion'
STATE = WORK / 'etapas.json'
SUMMARY = WORK / 'resultado.json'
PYTHON = sys.executable
JAVA = Path('C:/Program Files/Eclipse Adoptium/jdk-17.0.20.8-hotspot/bin')
SITES = Path.home() / '.codex/plugins/cache/openai-curated-remote/sites/0.1.75/scripts/site-workflow.mjs'
# HUELLAS: una etapa sólo se reutiliza si sus entradas y salidas siguen iguales.
def sha(path: Path) -> str:
value = hashlib.sha256()
with path.open('rb') as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
value.update(chunk)
return value.hexdigest()
def files(paths):
result = []
for path in map(Path, paths):
if path.is_dir():
result.extend(p for p in path.rglob('*') if p.is_file() and '__pycache__' not in p.parts)
elif path.is_file():
result.append(path)
else:
raise FileNotFoundError(path)
return sorted(set(result))
def digest(paths) -> str:
return hashlib.sha256(json.dumps([(str(p), sha(p)) for p in files(paths)]).encode()).hexdigest()
def write_json(path, value):
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + '.tmp')
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
temporary.replace(path)
# HERRAMIENTAS: usar ejecutables reales evita los problemas de .cmd y comillas.
def executable(name):
found = shutil.which(name + '.exe') or shutil.which(name)
if found and Path(found).suffix.lower() == '.cmd':
match = re.search(r'"([^"\r\n]+\.exe)"', Path(found).read_text(encoding='utf-8'))
if match and Path(match[1]).is_file():
return match[1]
if not found:
raise RuntimeError('No se encontró ' + name)
return found
def ps(script, *arguments):
return [executable('pwsh'), '-NoProfile', '-File', str(ROOT / script), *map(str, arguments)]
def build_sources():
return [ROOT / 'src', ROOT / 'build.gradle', ROOT / 'settings.gradle', ROOT / 'gradle.properties',
ROOT / 'gradle/wrapper', ROOT / 'gradlew.bat', ROOT / 'compilar.ps1']
def server_test_sources():
backend = [p for p in (ROOT / 'src/main/java').rglob('*.java')
if '/horror/client/' not in p.as_posix() and p.name != 'HostSkinAtlas.java']
infrastructure = [p for p in build_sources() if p != ROOT / 'src']
resources = [p for p in (ROOT / 'src/main/resources').rglob('*')
if p.is_file() and '/textures/' not in p.as_posix() and '/sounds/' not in p.as_posix()]
return backend + infrastructure + resources + [ROOT / 'config/raps']
def stop_owned(process):
# Sólo terminar el árbol que inició este publicador, nunca Java/Chrome por nombre.
if process.poll() is None:
if os.name == 'nt':
subprocess.run(['taskkill', '/PID', str(process.pid), '/T', '/F'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15)
else:
process.kill()
process.wait(timeout=15)
# EJECUCIÓN: límite total, registro por etapa y avance breve cada 30 segundos.
def run(command, log, limit=600, cwd=ROOT, input_text=None):
environment = os.environ.copy()
git_bin = Path('C:/Program Files/Git/bin')
if git_bin.exists():
environment['PATH'] = str(git_bin) + os.pathsep + environment.get('PATH', '')
if any('site-workflow.mjs' in str(part) for part in command):
# GNU tar de Git Bash interpreta C: como host remoto si no se fuerza ruta local.
environment['TAR_OPTIONS'] = '--force-local'
environment['PYTHONUNBUFFERED'] = '1'
started = time.monotonic()
next_notice = started + 30
check_failure_at = started + 1
game_tests = any('runGameTestServer' in str(part) for part in command)
with Path(log).open('w', encoding='utf-8') as stream:
process = subprocess.Popen(list(map(str, command)), cwd=cwd, env=environment,
stdin=subprocess.PIPE if input_text is not None else subprocess.DEVNULL,
stdout=stream, stderr=subprocess.STDOUT,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
try:
if input_text is not None:
process.stdin.write(input_text.encode('utf-8'))
process.stdin.close()
while process.poll() is None:
now = time.monotonic()
if now - started > limit:
raise TimeoutError(f'La etapa excedió {limit}s. Registro: {log}')
if game_tests and now >= check_failure_at:
stream.flush()
current = Path(log).read_text(encoding='utf-8', errors='replace')
if re.search(r'\[minecraft/LogTestReporter\].* failed!', current):
raise RuntimeError('Forge informó una prueba fallida; publicación detenida. Registro: ' + str(log))
check_failure_at = now + 1
if now >= next_notice:
print(f' {Path(log).stem}: {int(now - started)}s', flush=True)
next_notice = now + 30
time.sleep(.2)
if process.returncode:
tail = Path(log).read_text(encoding='utf-8', errors='replace').splitlines()[-12:]
raise RuntimeError(f'Etapa fallida ({process.returncode}): {log}\n' + '\n'.join(tail))
finally:
stop_owned(process)
return time.monotonic() - started
class Publisher:
def __init__(self, force=False, work=WORK):
self.work = work
self.work.mkdir(parents=True, exist_ok=True)
self.state_path = work / 'etapas.json'
self.state = json.loads(self.state_path.read_text(encoding='utf-8')) if self.state_path.exists() else {}
self.force = force
self.done = []
def phase(self, name, command, inputs, outputs=(), limit=600, before=None, after=None):
key = digest(inputs)
invocation = list(map(str, command))
hooks = [inspect.getsource(hook) if hook is not None else None for hook in (before, after)]
output_paths = list(map(Path, outputs))
previous = self.state.get(name, {})
if (not self.force and previous.get('inputs') == key
and previous.get('command') == invocation and previous.get('hooks') == hooks
and all(p.exists() for p in output_paths)
and previous.get('outputs') == {str(p): sha(p) for p in output_paths}):
print(name + ': reutilizado', flush=True)
self.done.append({'stage': name, 'status': 'cached'})
return
print(name + ': iniciando', flush=True)
if before:
before()
seconds = run(command, self.work / (name + '.log'), limit)
if after:
after(self.work / (name + '.log'))
if digest(inputs) != key:
raise RuntimeError('Cambió una entrada durante ' + name + '; retomar con las fuentes estables.')
self.state[name] = {'inputs': key, 'command': invocation, 'hooks': hooks,
'outputs': {str(p): sha(p) for p in output_paths},
'seconds': round(seconds, 2), 'completed': time.strftime('%Y-%m-%dT%H:%M:%S')}
write_json(self.state_path, self.state)
self.done.append({'stage': name, 'status': 'passed', 'seconds': round(seconds, 2)})
print(name + ': completo', flush=True)
# PRUEBAS DE SERVIDOR: conservar el mundo local anterior y exigir el resultado Forge.
def prepare_test_world():
world = (ROOT / 'run/world').resolve()
if world.parent != (ROOT / 'run').resolve():
raise RuntimeError('Ruta de mundo inesperada')
if world.exists():
world.rename(world.with_name('world-before-release-' + time.strftime('%Y%m%d-%H%M%S')))
def check_game_tests(log):
text = log.read_text(encoding='utf-8', errors='replace')
match = re.search(r'All (\d+) required tests passed', text)
if not match or 'BUILD SUCCESSFUL' not in text:
raise RuntimeError('Forge no confirmó todas las pruebas requeridas: ' + str(log))
write_json(WORK / 'pruebas-servidor.json', {'passed': True, 'required_tests': int(match[1])})
# INSTALACIÓN: respaldar el JAR anterior y comparar el archivo copiado.
def install_jar():
source = ROOT / 'build/libs/Raps.jar'
with zipfile.ZipFile(source) as archive:
if archive.testzip() is not None:
raise RuntimeError('El JAR contiene una entrada dañada')
for target in [ROOT / 'dist/Raps.jar', ROOT.parent / 'mods/Raps.jar']:
target.parent.mkdir(parents=True, exist_ok=True)
if target.exists() and sha(target) != sha(source):
backup = ROOT / 'respaldos' / ('publicacion-' + time.strftime('%Y%m%d-%H%M%S')) / target.parent.name
backup.mkdir(parents=True, exist_ok=True)
shutil.copy2(target, backup / target.name)
temporary = target.with_suffix('.jar.tmp')
shutil.copy2(source, temporary)
if sha(temporary) != sha(source):
raise RuntimeError('Copia incorrecta: ' + str(target))
temporary.replace(target)
return sha(source)
# INFORME: documentación de la entrega tomada de los resultados, sin cifras fijas.
def delivery_report(jar_hash, server_tests_skipped=False):
def read(relative):
path = ROOT / relative
return json.loads(path.read_text(encoding='utf-8')) if path.exists() else {}
server = read('build/publicacion/pruebas-servidor.json')
motion = read('docs/parasito-movimiento-validacion.json')
native = read('build/jar-native-audit/result.json')
remote = read('build/publicacion/exaroton.json')
remote_current = remote.get('status') == 'verified' and remote.get('sha256') == jar_hash
release_state = json.loads(STATE.read_text(encoding='utf-8'))
report = {
'created': release_state['compilar']['completed'], 'jar_sha256': jar_hash,
'jar_bytes': (ROOT / 'build/libs/Raps.jar').stat().st_size,
'rig_sha256': sha(ROOT / 'src/main/java/ar/com/companeros/horror/client/ParasiteRig.java'),
'model_sha256': sha(ROOT / 'src/main/java/ar/com/companeros/horror/client/ParasiteModel.java'),
'server_tests': {'status': 'skipped'} if server_tests_skipped else server,
'locomotion': motion,
'native_source_equivalence': native,
'exposed_skin': read('build/exposed-skin-audit/result.json'),
'continuity': read('build/motion-support-audit/current.json'),
'media': read('docs/parasito-preview-validacion.json'),
'voice_studio': read('docs/voice-studio/validacion.json'),
'exaroton': remote if remote_current else {'status': 'not_verified_for_this_jar'}
}
write_json(ROOT / 'docs/entrega-actual.json', report)
count = server.get('required_tests', 'sin registro')
tests = 'Pruebas de servidor omitidas en esta ejecución.' if server_tests_skipped else f'Forge confirmó {count} pruebas requeridas.'
remote_text = ('El JAR de Exaroton fue descargado y su hash coincide con esta entrega. El servidor permanece apagado.'
if remote_current else 'La instalación de Exaroton no está verificada para este JAR.')
markdown = f'''# Entrega actual de Raps
Generada el {report['created']} desde los resultados de esta compilación.
JAR: **{report['jar_bytes']} bytes**, SHA256 `{jar_hash}`.
{tests} La auditoría de locomoción comprobó {motion.get('checked_samples', 'sin registro')} muestras en {', '.join(motion.get('selected_modes', []))}. La equivalencia fuente/JAR verificó {native.get('native_cli', {}).get('native_frames_per_target', 'sin registro')} muestras nativas. Los informes completos están en [entrega-actual.json](entrega-actual.json).
El acecho ya no usa un apoyo ficticio. Las palmas consultan colisiones reales; adquirir y abandonar el apoyo es gradual. El plegado mezcla orientaciones continuas de los brazos y conserva la posición final del arrastre. La revisión de contorsiones sigue requiriendo observación dentro del cliente; la auditoría de locomoción no afirma ausencia absoluta de intersecciones en todas las poses.
Luna puede solicitar `auto`, `hunched`, `all_fours`, `crawl` y `wall_brace`. Las preferencias caducan y el entorno decide si son posibles. La cadencia, el presupuesto y el Gateway permanecen vigentes.
El estudio local transforma grabaciones e incorpora síntesis Piper bajo demanda. El preset utiliza el tratamiento de la voz aprobado; aplicar el mismo filtro a dos voces no garantiza identidad vocal idéntica.
El publicador instala el JAR local con respaldo y prepara las galerías. {remote_text}
'''
(ROOT / 'docs/ENTREGA-ACTUAL.md').write_text(markdown, encoding='utf-8')
def check_server_ready(server_tests_required=True):
state = json.loads(STATE.read_text(encoding='utf-8'))
jar = ROOT / 'build/libs/Raps.jar'
required_stages = [('compilar', build_sources())]
if server_tests_required:
required_stages.append(('pruebas-servidor', server_test_sources()))
for name, inputs in required_stages:
stage = state.get(name, {})
if stage.get('inputs') != digest(inputs):
raise RuntimeError('Las fuentes cambiaron después de ' + name)
if any(not Path(p).is_file() or sha(Path(p)) != value for p, value in stage.get('outputs', {}).items()):
raise RuntimeError('El artefacto cambió después de ' + name)
audit = json.loads((ROOT / 'build/jar-native-audit/result.json').read_text(encoding='utf-8'))
if not audit.get('passed') or audit.get('jar_sha256') != sha(jar):
raise RuntimeError('Falta comprobar la equivalencia del JAR actual con las fuentes')
print('Entrega actual compilada, probada y coincidente con su fuente.')
# SITES: preparar el mismo paquete; la credencial sólo entra por stdin.
def sites_package():
payload = json.loads(sys.stdin.readline())
credential = payload['credential']
hosting = json.loads((ROOT / 'galeria-chatgpt/.openai/hosting.json').read_text(encoding='utf-8'))
project_id = hosting['project_id']
checkout = ROOT / 'galeria-chatgpt'
command = [executable('node'), str(SITES), '--project-id', project_id]
opening_log = WORK / 'sites-opening.log'
run(command, opening_log, 180, checkout, json.dumps({'credential': credential}) + '\n')
source = next(json.loads(line) for line in opening_log.read_text(encoding='utf-8').splitlines()
if line.startswith('{') and 'checkout_path' in line)
# Archivos de publicación fuera del checkout: nunca se añaden al commit del sitio.
archive = WORK / 'galeria-chatgpt.tar.gz'
publishing_log = WORK / 'sites-package.log'
run(command, publishing_log, 600, checkout,
json.dumps({'credential': credential, 'source': source, 'archivePath': archive.as_posix()}) + '\n')
result = next(json.loads(line) for line in publishing_log.read_text(encoding='utf-8').splitlines()
if line.startswith('{') and 'archive' in line)
write_json(WORK / 'sites-paquete.json', result)
print(json.dumps(result), flush=True)
def self_test():
# El fixture usa sólo archivos temporales; no compila, instala ni publica.
import tempfile
with tempfile.TemporaryDirectory(prefix='raps-publicador-') as folder:
base = Path(folder)
entry, output = base / 'input.txt', base / 'output.txt'
entry.write_text('uno', encoding='utf-8')
publisher = Publisher(work=base)
command = [PYTHON, '-c', 'from pathlib import Path; Path(r"' + str(output) + '").write_text("ok")']
publisher.phase('fixture', command, [entry], [output])
publisher.phase('fixture', command, [entry], [output])
assert publisher.done[-1]['status'] == 'cached'
output.write_text('alterado', encoding='utf-8')
publisher.phase('fixture', command, [entry], [output])
assert output.read_text() == 'ok' and publisher.done[-1]['status'] == 'passed'
changed_command = [PYTHON, '-c', 'from pathlib import Path; Path(r"' + str(output) + '").write_text("nuevo")']
publisher.phase('fixture', changed_command, [entry], [output])
assert output.read_text() == 'nuevo' and publisher.done[-1]['status'] == 'passed'
entry.write_text('dos', encoding='utf-8')
publisher.phase('fixture', command, [entry], [output])
try:
publisher.phase('fallo', [PYTHON, '-c', 'raise SystemExit(7)'], [entry])
except RuntimeError:
pass
else:
raise AssertionError('No bloqueó una etapa fallida')
assert 'fallo' not in publisher.state
try:
run([PYTHON, '-c', 'import time; time.sleep(10)'], base / 'timeout.log', .4)
except TimeoutError:
pass
else:
raise AssertionError('No aplicó el límite de tiempo')
print('Autoprueba: caché, invalidación, fallo y tiempo límite aprobados.')
# ENTREGA: las etapas posteriores nunca corren si una validación anterior falla.
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--prepare-only', action='store_true')
parser.add_argument('--force', action='store_true')
parser.add_argument('--skip-server-tests', action='store_true')
parser.add_argument('--self-test', action='store_true')
parser.add_argument('--sites-package', action='store_true')
parser.add_argument('--write-report', action='store_true')
parser.add_argument('--server', action='store_true')
parser.add_argument('--server-only', action='store_true')
parser.add_argument('--check-server-ready', action='store_true')
args = parser.parse_args()
WORK.mkdir(parents=True, exist_ok=True)
if args.self_test:
self_test()
return
if args.sites_package:
sites_package()
return
if args.check_server_ready:
check_server_ready()
return
if args.server_only:
run(ps('publicar-servidor-raps.ps1'), WORK / 'exaroton.log', 300)
return
if args.write_report:
delivery_report(sha(ROOT / 'build/libs/Raps.jar'), args.skip_server_tests)
return
publisher = Publisher(args.force)
lock = (WORK / 'publicador.lock').open('a+b')
if os.name == 'nt':
import msvcrt
lock.seek(0)
try:
msvcrt.locking(lock.fileno(), msvcrt.LK_NBLCK, 1)
except OSError:
raise RuntimeError('Ya hay un publicador activo')
try:
source = ROOT / 'src'
jar = ROOT / 'build/libs/Raps.jar'
# Estas herramientas viven fuera de build para sobrevivir a Gradle clean.
required = ['docs/publicacion/verify-final-jar.py', 'docs/publicacion/JarRigExportAudit.java',
'docs/publicacion/run-exposed-skin-audit.py', 'docs/publicacion/ExposedSkinAudit.java',
'docs/publicacion/check-published-gallery.cjs']
for relative in required:
if not (ROOT / relative).is_file():
raise RuntimeError('Falta una herramienta de publicación: ' + relative)
build_inputs = build_sources()
publisher.phase('compilar', ps('compilar.ps1', '-Tarea', 'build', '-SinRed'), build_inputs, [jar], 900)
if not args.skip_server_tests:
publisher.phase('pruebas-servidor', ps('compilar.ps1', '-Tarea', 'runGameTestServer', '-SinRed'),
server_test_sources(), [WORK / 'pruebas-servidor.json'], 900,
prepare_test_world, check_game_tests)
rig = source / 'main/java/ar/com/companeros/horror/client/ParasiteRig.java'
model = source / 'main/java/ar/com/companeros/horror/client/ParasiteModel.java'
textures = source / 'main/resources/assets/companeros/textures/entity'
# El auditor consume clases propias del visor, distintas de las de Gradle.
preview_classes = ROOT / 'build/parasite-preview-classes'
preview_classes.mkdir(parents=True, exist_ok=True)
publisher.phase('compilar-visor', [JAVA / 'javac.exe', '-encoding', 'UTF-8', '-d', preview_classes, rig],
[rig], [preview_classes / ('ar/com/companeros/horror/client/ParasiteRig' + name + '.class')
for name in ['', '$Pose', '$Bone', '$Box', '$State', '$Bounds']], 90)
publisher.phase('movimiento', [PYTHON, ROOT / 'docs/validate-parasite-preview.py', '--modes', 'stalk,crawl3,climb'],
[rig, ROOT / 'docs/validate-parasite-preview.py'], [ROOT / 'docs/parasito-movimiento-validacion.json'], 240)
native_audit = ROOT / 'docs/publicacion/verify-final-jar.py'
publisher.phase('fuente-jar', [PYTHON, native_audit, '--jar', jar, '--source-only'],
[rig, jar, native_audit, ROOT / 'docs/publicacion/JarRigExportAudit.java'],
[ROOT / 'build/jar-native-audit/result.json'], 240)
skin_audit = ROOT / 'docs/publicacion/run-exposed-skin-audit.py'
publisher.phase('piel', [PYTHON, skin_audit],
[source / 'main/java/ar/com/companeros/identity/HostSkinAtlas.java', skin_audit,
ROOT / 'docs/publicacion/ExposedSkinAudit.java', ROOT / 'docs/skin-huesped-neon.png'],
[ROOT / 'build/exposed-skin-audit/result.json'], 180)
modes = ['idle', 'lore', 'chase', 'chase4', 'drag', 'crawl', 'crawl2', 'crawl3', 'crawl4', 'fold', 'turn', 'climb', 'attack', 'friendly']
media_outputs = [ROOT / 'docs' / ('parasito-' + mode + '.mp4') for mode in modes]
media_outputs += [ROOT / 'docs/parasito-modelos-origen.json', ROOT / 'docs/parasito-modelos.png',
ROOT / 'docs/modelos-3d/parasito-humanoide.obj', ROOT / 'docs/modelos-3d/parasito-aracnido.obj']
stills = ['rostro', 'evolucion', 'sensible', *['fase-' + str(n) for n in range(5)],
*['prefab-' + material for material in ['obsidian', 'iron', 'diamond', 'copper']]]
media_outputs += [ROOT / 'docs' / ('parasito-' + name + '.png') for name in stills]
media_outputs += [ROOT / 'docs' / name for name in ['parasito-modelos.html', 'parasito-animaciones.mp4',
'parasito-chase.gif', 'parasito-lore.gif', 'parasito-encuentro-voz.mp4', 'parasito-encuentro-completo.mp4']]
objects = ['parasito-humanoide', 'parasito-encuentro', 'parasito-aracnido', 'parasito-sensible', 'parasito-huesped-inicial']
media_outputs += [ROOT / 'docs/modelos-3d' / (name + '.' + ext) for name in objects for ext in ['obj', 'json']]
media_outputs += [ROOT / 'docs/modelos-3d' / (name + '.' + ext)
for name in ['parasite', 'parasite_initial', 'parasite_friendly'] for ext in ['mtl', 'png']]
media_outputs += [ROOT / 'docs/modelos-3d' / ('parasito-prefab-' + name + '.obj') for name in ['obsidian', 'iron', 'diamond', 'copper']]
publisher.phase('medios', [PYTHON, ROOT / 'docs/render-parasite-preview.py'],
[rig, model, textures, ROOT / 'docs/render-parasite-preview.py', ROOT / 'docs/parasito-voz.mp3'],
media_outputs, 1200)
publisher.phase('corredor', [PYTHON, ROOT / 'docs/render-corridor-demo.py'],
[rig, model, textures, ROOT / 'docs/render-corridor-demo.py', ROOT / 'docs/render-parasite-preview.py'],
[ROOT / 'docs/parasito-crawl3-paredes-origen.json', ROOT / 'docs/parasito-crawl3-paredes.mp4'], 300)
publisher.phase('escalada', [PYTHON, ROOT / 'docs/render-climbing-demo.py', '--expected-rig-sha256', sha(rig)],
[rig, model, textures, ROOT / 'docs/render-climbing-demo.py', ROOT / 'docs/render-parasite-preview.py'],
[ROOT / 'docs/parasito-climb-pared-origen.json', ROOT / 'docs/parasito-climb-pared.mp4'], 300)
publisher.phase('validar-medios', [PYTHON, ROOT / 'docs/validate-parasite-assets.py'],
[rig, model, ROOT / 'docs/validate-parasite-assets.py'] +
[p for p in (ROOT / 'docs').glob('parasito-*') if p.name != 'parasito-preview-validacion.json'],
[ROOT / 'docs/parasito-preview-validacion.json'], 300)
# Revalidar antes de instalar: otro proceso pudo cambiar fuentes durante los renders.
check_server_ready(not args.skip_server_tests)
jar_hash = install_jar()
if args.server:
if args.prepare_only or args.skip_server_tests:
raise RuntimeError('Publicar el servidor requiere una entrega con pruebas y publicación habilitadas.')
run(ps('publicar-servidor-raps.ps1'), WORK / 'exaroton.log', 300)
delivery_report(jar_hash, args.skip_server_tests)
run(ps('publicar-visor.ps1', '-SoloPreparar'), WORK / 'preparar-web.log', 300)
run(ps('preparar-galeria.ps1'), WORK / 'escritorio.log', 180)
run(ps('preparar-auditoria-sites.ps1'), WORK / 'preparar-sites.log', 180)
site_inputs = [ROOT / 'dist/visor']
if not args.prepare_only:
wrangler = Path.home() / 'Desktop/Rapsodia/lista-azul-web/node_modules/wrangler/bin/wrangler.js'
for branch in ['main', 'preview']:
publisher.phase('cloudflare-' + branch,
[executable('node'), wrangler, 'pages', 'deploy', ROOT / 'dist/visor',
'--project-name', 'raps-huesped', '--branch', branch, '--commit-dirty=true'],
site_inputs + [wrangler], limit=300)
run([executable('node'), ROOT / 'docs/publicacion/check-published-gallery.cjs', '--cloudflare-only'],
WORK / 'verificar-web.log', 300)
result = {'status': 'prepared' if args.prepare_only else 'published', 'jar_sha256': jar_hash,
'jar': str(jar), 'server_tests': 'skipped' if args.skip_server_tests else 'passed',
'stages': publisher.done, 'sites': {'status': 'prepared-for-authenticated-package',
'command': [PYTHON, str(Path(__file__)), '--sites-package']},
'urls': ['https://raps-huesped.pages.dev/?v=' + jar_hash[:8] + '#movimiento',
'https://preview.raps-huesped.pages.dev/?v=' + jar_hash[:8] + '#movimiento']}
write_json(SUMMARY, result)
print(json.dumps(result, ensure_ascii=False), flush=True)
except Exception as error:
write_json(SUMMARY, {'status': 'failed', 'error': str(error), 'completed_stages': publisher.done})
raise
finally:
lock.close()
if __name__ == '__main__':
main()