El botón de commit del editor es un botón de deploy
Se suponía que esto sería una función directa: editar los metadatos y el cuerpo de un post en el editor de blog-manager, y luego confirmar ese borrador de vuelta al repositorio del blog. Lo que lo volvió interesante fue una pregunta que casi se pasó por alto.
Lo que se buscaba hacer
Blog-manager ya tenía un flujo de borrador funcional: abrir un post, traer su frontmatter y cuerpo desde GitHub a una fila de borrador, editarlo en el navegador, autoguardar en la base de datos. Lo que no tenía era una forma de publicar esas ediciones. Esta era la pieza faltante: validar el borrador, escribirlo de vuelta al archivo en GitHub, resincronizar el registro Post, y limpiar el borrador.
La pregunta que reformuló el plan
Mientras se planeaba la cuestión de sync vs. async (si el commit debía correr en línea dentro de la solicitud, o delegarse a un job en segundo plano como la mayoría de las escrituras a GitHub de esta app), hubo una pausa en una sola línea: ¿qué significa exactamente “confirmar un borrador”? Confirmar a main dispara un deploy.
Ese replanteamiento fue el correcto. Hasta ese momento, “confirmar a la rama por defecto del blog” se había tratado como una operación de git con las consecuencias de una operación de git. No lo es. Tanto vdaluz.com como imperfectsystems.com se despliegan vía Cloudflare Workers Builds, que despliega automáticamente en cada push a main. Un commit desde este editor es un deploy de producción en vivo, inmediato y de un solo sentido, no un job de sincronización en segundo plano con cola de reintentos.
Una vez resuelto eso, correr el commit de forma síncrona se volvió la elección obvia. Un job en segundo plano significa hacer clic en “Commit,” la página dice algo poco concluyente, y treinta segundos después o el post está en vivo o algo falló en silencio. Para una acción con consecuencias de deploy, esa brecha es un riesgo. Correrlo en línea significa que la solicitud devuelve un SHA de commit o un error específico y accionable, antes de que alguien se aleje pensando que funcionó.
La interfaz refleja eso: el botón de commit lleva un diálogo de confirmación que dice sin rodeos, “¿Confirmar y desplegar en vivo a
Lo que se construyó
El committer hace cinco cosas en orden: rechaza la operación si el escaneo del blog está corriendo en ese momento (para evitar competir con una lectura concurrente de GitHub), valida los metadatos del borrador, vuelve a obtener el archivo en vivo y compara su SHA contra el que tenía el borrador al abrirse (detección de conflictos sin columna extra, ya que una coincidencia de SHA al momento del commit es en sí misma prueba de que el punto de partida del borrador todavía coincide con GitHub), calcula la diferencia solo de los campos que realmente se tocaron para que el formato YAML no tocado sobreviva intacto, luego hace un PUT del archivo, resincroniza las columnas del Post desde el nuevo frontmatter, y destruye el borrador.
Ese detalle de “solo los campos cambiados” importó más de lo esperado. El frontmatter del borrador pasa por una columna JSON, así que un pubDate guardado vuelve como un String, mientras que un parseo nuevo del archivo en vivo produce un Date real. Una comparación de igualdad ingenua vería esos como diferentes y reescribiría el formato YAML de la fecha aunque nadie la hubiera tocado. Se escribió una prueba exactamente para eso, y luego se rompió la corrección a propósito para confirmar que la prueba en verdad la detectaba antes de confiar en ella.
Lo que encontró la revisión
Tres ángulos de revisión independientes convergieron en la misma línea, lo cual suele ser señal de que hay algo real ahí: el nuevo validador revisaba la lista completa de affiliates de un borrador contra el conjunto fijo de programas conocidos. Pero esta app tiene un caso especial existente que deliberadamente permite que un post conserve un programa de afiliado heredado, anterior a la lista fija, sin validarlo nunca. La nueva ruta de validación no sabía de ese caso especial, así que cualquier post que llevara un afiliado heredado fallaría al confirmarse incluso en una edición que nunca tocó los afiliados. Se corrigió validando solo la intersección con el conjunto conocido, mientras se seguía escribiendo el valor almacenado completo (programa heredado incluido) de vuelta al archivo. Se agregó una prueba de regresión con un fixture real de afiliado heredado para fijarlo.
Otras dos cosas menores salieron de la misma pasada: el paso del cargador de borradores al parser de frontmatter compartido cambió el comportamiento para un archivo con un bloque de frontmatter presente pero en blanco, de tolerarlo en silencio a lanzar una excepción, y la cláusula rescue del controlador no se había actualizado para capturar el nuevo tipo de excepción, así que ese caso daba un 500 en vez de una redirección amigable. Y la lógica de resincronización posterior al commit había derivado en un casi duplicado del propio código de mapeo de campos del scanner, así que se llevó a un solo método compartido que ambos llaman.
Vale la pena nombrar como lección general: una segunda ruta de escritura que valida el estado completo fusionado en vez de solo lo que cambió romperá cualquier caso especial establecido de “conservar el valor heredado, no validarlo” en cualquier otra parte del código. Se documentó esto como su propia nota de base de conocimiento, ya que es un patrón que vale la pena vigilar más allá de esta función puntual.
Lo que no se verificó
Los datos de desarrollo intencionalmente no tienen un token de GitHub con permiso de escritura, así que nunca se ejerció un commit exitoso real de punta a punta: un PUT real, un deploy real, una resincronización real del Post desde una respuesta de commit real. Esa ruta solo está cubierta por pruebas unitarias con cliente simulado. La verificación en navegador confirmó el cableado de la interfaz y las rutas de guardia, conflicto y error contra una solicitud real (que falla por autenticación). El primer commit en vivo a través de este flujo será la primera vez que corra de verdad el camino feliz completo.
Lecturas relacionadas
El bug de normalización que solo aparece con etiquetas hechas de nada
Un normalizador basado en strip se topa con una etiqueta de puro signo de puntuación: string vacío como clave de hash, sustitución de etiqueta equivocada, y un autocompletado que hace match con todo. Tres síntomas, una sola causa raíz.
La misma decisión de botón me costó un bug más grande de lo esperado
Incrustar el flujo de imagen destacada en el editor parecía la opción más chica, hasta que 'reemplazar' se topó con 166 archivos reales que nunca habían pasado por el camino de solo inserción, y una migración sin backfill.
Operaciones globales de tags, y el guard de concurrencia que no lo era
Un renombrado en lote que reportaba éxito mientras fallaban posts, una clave limits_concurrency que nunca compartió un lock de verdad, y 465 tags reales con un duplicado de mayúsculas en vivo para probar contra él.