16 · Operations runbook¶
Estado del documento
Versión: 1.0 · 17-may-2026 Estado: ✅ completo Audiencia: Equipo de desarrollo
1. Para qué sirve este documento¶
Runbook operacional del portal B2B Outlet: los procedimientos de mantenimiento, despliegue y resolución de incidencias una vez el portal está en producción. Es el documento de referencia para "algo hay que hacer/tocar/arreglar en el sistema vivo".
Cierra el eje de Desarrollo. Donde los docs 00–15 explican cómo está construido cada componente, este explica cómo se opera. Cuando un procedimiento ya está documentado en otro doc, este remite en vez de duplicar.
Lectores principales: cualquier dev o IA que tenga que desplegar un cambio, rotar un secret, diagnosticar un fallo en producción, atender una petición de reclasificación de catálogo, o preparar el traspaso al cliente.
2. Modelo de despliegue¶
El repo deploya solo¶
main está conectada al theme live vía la integración nativa GitHub↔Shopify. Cualquier commit a main que toque carpetas del theme se despliega al storefront de producción en ~30-60s. No hay staging. Merge a main = deploy a producción. Detalle en 12-github-repo §2.
Trabajo nuevo: feature branch → PR → merge. Nunca push directo a main con trabajo a medias.
Cambiar la rama conectada (operación delicada)¶
Shopify no permite "cambiar la rama de origen" de un theme conectado; el flujo real es desconectar y crear un theme nuevo apuntando a otra rama, lo que genera un theme ID distinto. Procedimiento:
- (Recomendado) Online Store → Themes → theme live → Duplicate. Queda como theme unpublished — rollback gratis.
- Disconnect del theme GitHub-conectado actual. Pasa a unpublished.
- Add theme → Connect from GitHub → repo + rama nueva. Crea un theme entry nuevo.
- Preview sobre
*.shopifypreview.compara validar sin publicar (con las limitaciones de §6). - Publish cuando esté correcto. El theme antiguo queda unpublished y accesible.
- Reconfirmar en las apps de theme (Locksmith) que apuntan al theme ID nuevo.
- Esperar varios días estable antes de borrar el theme antiguo.
Lo que no se ve afectado por el reconnect (vive a nivel shop, no theme): customers, productos, colecciones, páginas, navegación, metafields, flows, los HMAC secrets (viajan en config/settings_data.json), los endpoints de Supabase, y los locks/keys de Locksmith. Pero la app Locksmith sí necesita reapuntar al theme nuevo.
3. Despliegue de edge functions¶
Tras cambios de código o de supabase/config.toml:
El detalle de las 10 funciones y el flag verify_jwt está en 11-supabase. Lo crítico para operar:
Cuándo redeployar¶
- Cambio de código en
functions/<f>/index.ts. - Cambio en
config.toml(incluido el flagverify_jwt). - Rotación de un secret que lee la función. Sin redeploy, el container caliente sigue con el valor viejo en RAM. Esta es la causa #1 de bugs falsos tipo "el token ya lo cambié pero sigue fallando" — ver 14-secrets §6.
El flag verify_jwt y el deploy¶
supabase functions deploy lee config.toml y sobrescribe lo que el dashboard tenga configurado. La fuente de verdad del flag verify_jwt es el fichero, no el dashboard. Si una función pierde su verify_jwt correcto del fichero, el siguiente deploy rompe la integración (401 a nivel gateway, o función pública por error). Detalle en 11-supabase §4.
Logs¶
O Dashboard → Edge Functions → \<función> → Logs.
4. Rotación de SHOPIFY_ADMIN_TOKEN¶
El procedimiento más frecuente. El inventario completo de secrets y el resto de procedimientos de rotación están en 14-secrets; aquí el del token de Shopify por ser el más habitual:
- Shopify Admin → Settings → Apps and sales channels → Develop apps → \<app> → Configuration. Si añades scopes, márcalos y Save — los scopes nuevos NO se aplican al token vigente, hay que regenerarlo.
- API credentials → regenerar el token. Copia el nuevo (
shpat_…). - Verifica el token antes de propagarlo, con un
curla la Admin API:Debe devolvercurl -s -X POST \ -H "X-Shopify-Access-Token: <nuevo_token>" \ -H "Content-Type: application/json" \ -d '{"query":"{ shop { name } }"}' \ https://<shop>.myshopify.com/admin/api/2025-10/graphql.json{"data":{"shop":{"name":"..."}}}. Si devuelveInvalid API key or access token, el token está mal copiado. - Sobrescribe el secret en Supabase:
supabase secrets set SHOPIFY_ADMIN_TOKEN=<nuevo_token> --project-ref <project-ref>. - Redeploy de las 10 funciones (todas leen el token — ver
supabase/README.mdpara el comando completo). - Actualiza el valor también en
shopify-ledsc4-theme.envlocal y en los GitHub Actions secrets (SHOPIFY_ADMIN_TOKEN) — ver 14-secrets §6. - Smoke:
/pages/mis-solicitudescarga sin error 401,/pages/solicitudpermite enviar una solicitud.
5. Locksmith¶
Setup actual¶
El gate del storefront es híbrido (ver 04-storefront-gate). Locksmith cubre una de las tres reglas:
- Lock — scope producto + colección
all. Redirect on lock →/pages/cuenta-en-revision. - Key — condiciones
customer_signed_in+customer_tag = aprobado. Redirect URL vacío.
Gotcha — redirect_url en KEY vs LOCK¶
Locksmith permite poner redirect_url tanto en la lock como en la key, y hacen lo opuesto:
redirect_urlen la lock: se aplica a quien NO tiene la key (acceso denegado). Es el caso normal.redirect_urlen la key: se aplica a quien SÍ abre el lock (acceso concedido).
La key tuvo por error un redirect_url configurado, y el resultado fue que los customers aprobados eran redirigidos a "cuenta en revisión" cada vez que abrían una ficha de producto. Regla: la key siempre con Redirect URL vacío — su único trabajo es abrir el lock. Si se recrea la key, revisar este punto antes de publicar.
Si Locksmith deja de funcionar¶
Si la app falla (trial caducado, desinstalada, error), el gate Liquid de layout/theme.liquid cubre los casos importantes por sí solo — Locksmith es solo una capa redundante. Para desactivar Locksmith sin romper nada, comentar su include en layout/theme.liquid (es reversible cuando vuelva la app). Detalle del gate en 04-storefront-gate.
6. Limitaciones de preview¶
El gate del theme no dispara en dominios *.shopifypreview.com — es intencional, para poder previsualizar el theme sin que el gate redirija. Implicación: la validación funcional del gate solo se puede hacer tras publish, con el dominio real.
Relacionado: el redirect_uri post-OAuth de las cuentas de cliente se valida contra el dominio real de la tienda. Los dominios *.shopifypreview.com no están en la whitelist, así que cualquier flujo de login en preview falla con "redirect_uri no es una coincidencia válida". El login real solo se prueba en producción. Detalle en 05-registro-b2b.
7. Smoke test post-deploy¶
Checklist a validar tras cualquier deploy de theme o cambio en gate / Locksmith / edge functions. Se recorre con un customer de cada estado.
Anónimo¶
/→ home pública, sin header B2B./products/<handle>,/collections/<handle>,/cart,/search,/pages/solicitud,/pages/mis-solicitudes→ redirigen a/pages/acceso-profesional./pages/acceso-profesional,/pages/cuenta-en-revision,/pages/cuenta-rechazada, las páginas legales → accesibles.- "Iniciar sesión" en
/pages/acceso-profesional→ login real. "Solicitar acceso" → form de registro B2B.
Pendiente (logueado, sin tag aprobado/rechazado)¶
- Login → aterriza en
/pages/cuenta-en-revision. - Navegación a productos/colecciones/cart → redirige a cuenta-en-revision.
/pages/cuenta-en-revision→ muestra header simple + datos del customer.
Rechazado¶
- Login →
/pages/cuenta-rechazada. - Navegación a rutas comerciales → redirige a cuenta-rechazada.
Aprobado¶
- Login → aterriza en
/pages/mis-solicitudes. /→ header B2B + dashboard (no hero)./products/<handle>,/collections/all,/collections/coleccion-2026,/cart→ cargan sin redirect./pages/solicitudcon productos en el carrito → enviar → crea draft order, redirige a/pages/solicitud-enviada. Verificable en Admin → Orders → Drafts (tagssolicitud-b2b+pendiente-revision).- El email transaccional de solicitud recibida llega al customer.
8. Importer — operar un run¶
El pipeline de importación corre de forma autónoma (5 crons de sftp-sync + el workflow ledsc4-import.yml). El detalle está en 02b-importer-deploy y 13-github-actions §5. Lo relevante para operar:
Disparar un import manual¶
# kind heredado de la row de import_runs:
gh workflow run ledsc4-import.yml -f run_id=<uuid>
# forzar kind:
gh workflow run ledsc4-import.yml -f run_id=<uuid> -f kind_override=stock_only
El run_id debe ser de una row de private.import_runs en estado downloaded. Reprocesar un run ya completed/failed no es posible — hay que generar un run_id nuevo con una invocación fresca de sftp-sync.
Diagnóstico de un run¶
- Estado de los runs: tabla
private.import_runs(SQL editor o Studio con el schemaprivateexpuesto). FSMstarted → downloaded → processing → completed | failed. - Reports de un run: bucket Storage
ledsc4-imports, rutaruns/<run_id>/reports/. - Logs del workflow: pestaña Actions del repo → run de
ledsc4-import.yml.
Runs colgados en processing¶
Si el workflow muere entre "Mark run as processing" y el cierre (timeout de 60 min, cancelación), la row queda en processing indefinidamente. Se detectan con una query: status processing, started_at viejo, sin completed_at ni failed_at. No hay limpieza automática — hay que decidir caso por caso si el import llegó a aplicarse (mirar los reports en Storage) antes de re-disparar. Anotado como pendiente en 13-github-actions §8.
9. Metafield definitions bloqueadas por smart collections¶
Caso edge que aparece al correr scripts/apply-metafield-definitions.mjs.
Síntoma¶
El script reporta entradas en estado UpdateBlockedByDependency:
Causa¶
Shopify bloquea todos los campos de una metafield definition (description, access, pin, validations, type) mientras capabilities.smartCollectionCondition.enabled = true y existen smart collections que la usan como regla. La mutation metafieldDefinitionUpdate rechaza con CAPABILITY_CANNOT_BE_DISABLED. El script lo detecta a priori desde el flag de capability y reporta sin intentar el Update — por eso el dry-run refleja la realidad.
Hallazgo confirmado: el bloqueo aplica incluso a name/description, campos que no afectan a la condición. La única vía de edición conocida es el admin UI de Shopify (usa un endpoint interno distinto del Admin GraphQL público).
Cómo desbloquear¶
Tres caminos, de menor a mayor riesgo:
- Aceptar el statu quo: modificar el JSON para que coincida con el shop. El script reportará
Unchanged. Apropiado si el cambio era cosmético. - Cambiar el consumidor: si el storefront necesita leer el metafield con otro
access, modificar el snippet/sección Liquid para obtener el dato por otra vía, sin tocar el metafield ni las smart collections. - Eliminar la dependencia: quitar la condición del metafield de cada smart collection que lo use. Riesgo alto si las colecciones son user-facing — se pierde la organización del catálogo. Solo si esas colecciones también van a desaparecer.
Caso real resuelto: product.catalogo estaba bloqueada por las smart collections del outlet. Se editó vía admin UI (corrección de la descripción + access.storefront = PUBLIC_READ + pin). El detalle de la clasificación del script está en 15-scripts §4.
10. Reclasificación de categorías — política¶
El catálogo de categorías del portal (la jerarquía cat-*) se reorganizó en mayo 2026 (proyecto PR-CAT-RESTRUCTURE): el menú pasó de 6 a 5 categorías padre. Esa reestructuración introdujo un mecanismo de overrides por SKU que conviene entender antes de atender cualquier petición futura de recategorización.
El mecanismo de overrides¶
scripts/sku-overrides.json es una tabla de excepciones que pisa el valor del ERP para los metafields product.catalogo y product.tipo de productos concretos. Durante cada import nocturno, tras transformar el CSV y antes de escribir en Shopify, el importer consulta este fichero: si el producto está listado, su valor del CSV se sustituye por el del override. El ERP nunca se ve afectado; el portal queda con los valores override. Lo aplican scripts/lib/sku-overrides.mjs (cargador) e import-map.mjs (punto de aplicación) — ver 15-scripts §3.
Consecuencia: la fuente de verdad de la categorización está dividida. Para saber la categoría real de un producto override hay que mirar dos sitios — el feed del ERP y sku-overrides.json. Si difieren, gana el override. Cada SKU en ese fichero es un punto de discrepancia deliberada entre el portal y el ERP — es deuda técnica conocida.
Las tres vías para una petición de reclasificación¶
Cuando el cliente pida mover productos entre categorías, fusionar categorías o crear excepciones, hay tres vías, en orden de preferencia:
Vía 1 — Cambio en el ERP (preferida). Modificar la columna Catálogo o Tipo del producto en el ERP del cliente. El cambio se propaga al portal en el siguiente ciclo nocturno. No genera deuda técnica. Requiere coordinación con el equipo que gestiona el ERP.
Vía 2 — Añadir al fichero de overrides (aceptable para cambios pequeños). Editar scripts/sku-overrides.json, desplegar, esperar al siguiente cron. Acumula deuda técnica: cada SKU añadido es una discrepancia más entre portal y ERP.
Vía 3 — Edición manual en Shopify Admin (último recurso). Cambiar el metafield del producto desde el admin de Shopify. Esto se revierte cada noche cuando el cron reimporta — salvo que se combine con la Vía 2. No es una solución estable por sí sola.
Checklist al recibir una petición de reclasificación¶
- ¿Cuántos productos afecta? Menos de 10 → considerar Vía 2. Más → evaluar Vía 1.
- ¿Es permanente o temporal (campaña, estacionalidad)? Las temporales no deberían ir al override.
- ¿El cliente puede cambiarlo en su ERP? Si es "sí pero lento", el override es aceptable como puente, con compromiso de revertir cuando el ERP esté al día.
- ¿Afecta a la tipología, no solo al catálogo? Si sí, valorar inconsistencias visibles antes de aplicar — un producto puede acabar en una subcategoría cuyo nombre no concuerda con su título comercial.
Mantenimiento del fichero de overrides¶
Revisar sku-overrides.json al menos una vez por trimestre. Para cada regla, comprobar si sigue siendo necesaria o si el ERP ya refleja el estado deseado. Cuando una regla deja de hacer falta, eliminarla — la salida limpia siempre es preferible a la acumulación.
Reversión completa de PR-CAT-RESTRUCTURE¶
Si hubiera que deshacer la reestructuración entera (volver a las 6 categorías originales), el procedimiento es: vaciar sku-overrides.json a {"rules": []}, reactivar las categorías retiradas en setup-cat-collections.mjs y setup-cat-menu.mjs (el código exacto está en el commit 9d1dcf7), esperar al cron de las 02:00 UTC para que el importer reescriba los metafields con los valores del ERP, y re-ejecutar los dos scripts de categorías. Tiempo estimado: ~1 hora. Riesgo: las colecciones recreadas tendrán GIDs nuevos (los borrados no se recuperan) y sus traducciones se regeneran desde cero.
11. Transferencia al cliente¶
El portal se entrega a LedsC4 por hitos, no de golpe. La clasificación de qué secret genera el cliente y qué se transfiere está en 14-secrets §7. El handover por fases:
| Fase | Cuándo | Qué se transfiere |
|---|---|---|
| A | Cierre del proyecto | Documentación + acceso del cliente al repo (collaborator), al proyecto Supabase y al shop. Ownership sigue en Dani |
| B | ~3 meses tras la entrega | Transfer de ownership del repo de GitHub a la organización del cliente. Regenerar GITHUB_DISPATCH_TOKEN bajo el nuevo owner |
| C | Cuando el shop pase a producción | Transfer del shop de Shopify a la Partner account del cliente. Dani queda como collaborator |
| D | Cierre definitivo | Transfer del proyecto Supabase a la organización del cliente, vía la función nativa de Supabase (no se recrea) |
Checklist de cutover¶
Anotado por fase. Varios pasos solo aplican si el proyecto Supabase se recrea desde cero en vez de transferirse con la función nativa.
- [ ] (A) Compartir acceso al repo de GitHub con el cliente como collaborator.
- [ ] (A) Añadir al cliente como miembro del proyecto Supabase.
- [ ] (A) Configurar los 3 HMAC en
config/settings_data.jsonyCREATE_COMPANY_WEBHOOK_SECRETen el step Send HTTP request del Flow W2. - [ ] (A) Smoke test: invocar
sftp-syncy verificar host key + listing del SFTP. - [ ] (B) Transferir ownership del repo a la organización del cliente (GitHub Settings → Transfer ownership).
- [ ] (B) Verificar que el
repository_dispatchde Supabase aledsc4-import.ymlsigue funcionando tras el transfer (GitHub redirige, pero confirmar con unsftp-syncmanual). - [ ] (B) Regenerar
GITHUB_DISPATCH_TOKENbajo el nuevo owner y actualizarlo en Supabase secrets (ver el procedimiento de rotación en 14-secrets §6). - [ ] (B) Configurar/verificar los GitHub Actions secrets en el repo del cliente.
- [ ] (C) Transferir el shop a la Partner account del cliente. Dani queda como collaborator.
- [ ] (D) Transferir el proyecto Supabase a la organización del cliente (Project settings → Transfer project).
- [ ] (D) Si se recrea el proyecto en vez de transferirlo: aplicar las 10 migraciones (
supabase db push), setear los ~17 secrets manuales, los 2UPDATEdeprivate.config, re-deployar las 10 funciones, verificar los 6 crons. - [ ] (D) Documentar la fecha del cutover y eliminar las referencias al
project-refdel sandbox de desarrollo.
Qué no se transfiere¶
El shopify-ledsc4-theme.env local de Dani (el cliente reconstruye el suyo desde .env.example) y los tokens personales de Dani (Supabase CLI, GitHub PAT).
12. Pendientes conocidos¶
El proyecto mantiene un tracking de tareas no urgentes en docs/pendientes.md (material legacy, ver §13). Las que tienen relevancia operativa directa:
- Limpiar ~745 productos pre-existentes con handle basado en título, huérfanos tras el importer. Bloquea el cutover. Script one-shot que los identifique y archive/borre.
- Custom App sin scope
read_locations: el importer hace fallback a "la primera location". Funciona con una sola location, romperá si se añade otra. Fix: añadir el scope y regenerar el token. - Endurecer
promote-whitelist-matchesconX-Cron-Secret: hoy la función no valida auth (ver 11-supabase §9). - Limpiar emails de test de la whitelist antes del cutover (
test-bo1@example.com…test-bo5@example.com).
13. Material legacy y procedencia¶
Este runbook cosecha y reorganiza material que existía disperso antes de la estructura de documentación por ejes:
docs/operations-runbook.md— el runbook plano original. Su contenido vivo (deploy, edge functions, rotación, Locksmith, smoke test, metafields bloqueados) está refundido aquí, puesto al día.cierre-pr-cat-restructure.md— el documento de cierre del proyecto de reestructuración de categorías. Su política de reclasificación (Vía 1/2/3), el mecanismo de overrides y el procedimiento de reversión están refundidos en §10.docs/pendientes.md— el tracking de tareas. Las entradas con relevancia operativa están resumidas en §12.
Estos ficheros legacy están archivados en docs/_archive/ (PR #120, ver 12-github-repo §8). Este doc 16 es la fuente de verdad operativa; los ficheros planos son histórico.
14. Pendientes de este documento¶
-
Material legacy archivado.
docs/operations-runbook.md,docs/pendientes.mdy el resto del material plano están endocs/_archive/(PR #120). Elcierre-pr-cat-restructure.mdno está en el repo (era un documento de entrega externo); su contenido operativo ya está refundido en §10. -
El runbook no cubre incidencias del día a día del operador. Este doc es el runbook técnico (deploy, secrets, importer, infra). La operativa diaria del back-office — aprobar altas, gestionar whitelist, atender una incidencia de un cliente — vive en el eje
operador/. Para esa operativa, ver operador/04-resolucion-incidencias. -
Sin procedimiento de rollback de un import. §8 explica cómo diagnosticar un run colgado, pero no hay un procedimiento para deshacer un import que aplicó datos incorrectos a producción. El importer es incremental (fingerprint), así que un import malo se corrige con otro import corregido, pero conviene documentar el caso explícitamente.
-
Branch protection en
main(nota operativa). Mientras no esté activa (ver 12-github-repo §8), el modelo de despliegue de §2 depende de disciplina: un PR roto mergeado rompe producción sin que nada lo impida. Activarla es una recomendación operativa estándar para el modelomain→deploy de Shopify, a aplicar por quien gobierne el repo — no un entregable de cierre del proyecto.
Cambios¶
- v1.0 (17-may-2026): cabecera de estado añadida; documento ya estaba completo. Primera publicación del contenido: 17-may-2026.