D12 · Pipeline split: sftp-sync (Edge) → ledsc4-import.yml (GHA)¶
Estado del documento
Versión: 1.0 · 17-may-2026 Estado: ✅ aceptada Audiencia: Equipo de desarrollo
Estado¶
Aceptada · PR-A1 + PR-A2 (mayo 2026) · vigente.
Contexto¶
El importer ejecuta dos clases de trabajo con perfiles de ejecución muy distintos:
- Adquisición de datos — conexión SFTP al servidor del cliente, descarga de 8 CSVs (~4 MB total), persistencia en Supabase Storage, registro de run. Pesa poco computacionalmente (transferencia I/O), tarda segundos.
- Procesamiento + escritura — parsing de 745 productos × 6 idiomas, mapping a modelo Shopify, pre-upload de ~2700 imágenes (D11), mutaciones GraphQL
productSet+translationsRegister+publishablePublish. Tarda ~688s en run completo desde caché vacía, ~460s en re-runs.
Restricciones técnicas:
- Supabase Edge Functions tienen 60s de timeout (Deno Edge runtime). Imposible ejecutar el writer completo.
- Edge runtime no soporta
pgnativamente — el writer escribe enprivate.sku_stateyprivate.image_cacheconpg.Client, requiere conexión TCP completa. - Memoria limitada del Edge runtime — los 6 CSVs en memoria + modelo Shopify completo + buffers de imágenes superan los límites prácticos.
pg_cronsolo puede invocar SQL desde Postgres, no triggers HTTP directos. Necesita un edge function como puente.
Las dos clases de trabajo no pueden vivir en el mismo runtime. Y la primera tiene que vivir en Edge por la integración con pg_cron.
Decisión¶
Pipeline en dos componentes conectados por private.import_runs + repository_dispatch:
┌────────────────┐ ┌──────────────┐ ┌────────────────────┐
│ pg_cron │───▶│ sftp-sync │───▶│ ledsc4-import.yml │
│ (5 schedules) │ │ (Edge Fn) │ │ (GHA workflow) │
└────────────────┘ └──────────────┘ └────────────────────┘
│ │
▼ ▼
┌────────────────────────────────┐
│ private.import_runs (DB state) │
└────────────────────────────────┘
│ │
▼ ▼
┌────────────────────────────────┐
│ Storage: ledsc4-imports/runs/<id>│
│ ├─ inputs/productos/... │
│ ├─ inputs/stock/... │
│ ├─ inputs/precios/... │
│ └─ reports/... │
└────────────────────────────────┘
Responsabilidades¶
sftp-sync (Supabase Edge Function) — supabase/functions/sftp-sync/index.ts:
- Crea row en
private.import_runsconstatus='started',kind='full'|'stock_only'. - Conecta al SFTP del cliente, descarga los ficheros correspondientes al
kind. - Sube cada fichero a
ledsc4-imports/runs/<run_id>/inputs/...preservando la estructura de directorios (productos/,stock/,precios/). - Actualiza row:
status='downloaded',files=[{name, path_in_storage, size_bytes, sftp_mtime}, ...]. - Dispara
repository_dispatcha GHA conevent_type='ledsc4-import'yclient_payload={run_id}.
ledsc4-import.yml (GitHub Actions) — .github/workflows/ledsc4-import.yml:
- Fetch metadata del row vía
pg.Clientdirecto a Supabase. Validastatus='downloaded'. - Download files desde Storage usando los paths del campo
files. - Mark processing:
UPDATE status='processing' WHERE status='downloaded'. Conditional para prevenir re-disparos del mismorun_id. - Run writer: invoca
runFullImportorunStockOnlydescripts/import-write.mjscondbConnectionabierta. - Upload reports a
ledsc4-imports/runs/<run_id>/reports/. - Close run (always):
completedsi writer ok,failedsi stage='writer', sin tocar la row si la falla es pre-writer (operador puede re-disparar contra el mismorun_id).
Triggers del workflow¶
Dual-source:
workflow_dispatch— invocación manual desde UI ogh workflow run. Aceptarun_id(required) ykind_override(optional choice).repository_dispatch— invocación automática desdesftp-syncvíaPOST /repos/.../dispatchesconevent_type='ledsc4-import'yclient_payload={run_id}. Solorun_idviaja en el payload;kindse resuelve desde la row.
RUN_ID: ${{ inputs.run_id || github.event.client_payload.run_id }} — exactamente uno se popula por invocación.
Máquina de estados¶
started ──→ downloaded ──→ processing ──→ completed
│ │ │
│ │ └──→ failed (stage=writer|upload_reports)
│ │
│ └─ [pre-writer failure] → row intacta, GHA red
│
└─ [sftp-sync failure] → row marcada failed con error_stage
Solo downloaded es procesable. Reruns requieren fresh run_id (nueva invocación de sftp-sync).
Alternativas consideradas¶
Writer en Edge Function. Descartada por las restricciones de plataforma (60s timeout, sin pg, memoria limitada). El run completo tarda ~688s — imposible en Edge.
Writer en Supabase pgsql (procedure). Descartada: el writer hace llamadas a Shopify GraphQL, Shopify Files API, fetch externo a la CDN del cliente. Postgres no es runtime para eso.
Writer en runner self-hosted (VPS propio). Descartada por coste operativo (mantener VPS, deploys, monitoreo). GHA es runtime gratis con el plan actual.
Webhook directo de Storage → GHA (sin row de control). Descartada: sin estado intermedio en DB, no hay forma de: - Distinguir runs duplicados. - Reintentar un run fallido manualmente con el mismo input. - Auditar el histórico de qué se procesó y con qué resultado.
pg_cron invocando GHA directamente. Descartada: pg_cron no puede llamar HTTP externo. Necesita pasar por una función Postgres o un edge function intermedio. El edge function ya tiene que hacer el SFTP — añadir la responsabilidad de disparar el workflow es coste marginal.
Consecuencias¶
- Latencia entre disparo SFTP y arranque del writer: ~10-30s (tiempo de queue de GHA workflow + setup runner + npm install). Aceptable para un cron que tarda 10+ minutos.
- Dispatch best-effort:
repository_dispatchpuede fallar (rate-limit GHA API, transient errors). Mitigación: el row queda endownloaded, operador puede re-disparar manualmente conworkflow_dispatchpasando elrun_id. Documentado en 13-github-actions. - Conditional UPDATE en
mark_processing: previene race conditions si dos workflows aciertan a ejecutarse sobre el mismorun_id(escenario: dispatch best-effort + workflow_dispatch manual simultáneos). El segundo workflow verowCount=0y aborta con error. - Pre-writer failures dejan la row intacta (
status='downloaded'). El operador puede re-dispararworkflow_dispatchcontra el mismorun_idsin generar uno nuevo. Reduce coste de re-bajar del SFTP. - Reports siempre se intentan subir (
if: always()). Incluso runs fallidos producen logs accesibles desde Supabase Storage para debug. - Secrets viven en dos planos:
- Supabase (
sftp-sync): credenciales SFTP,GITHUB_TOKENpararepository_dispatch,supabase_anon_keypara llamarse a sí mismo desdepg_cron. - GHA secrets (workflow):
SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY,SUPABASE_DB_URL,SHOPIFY_SHOP,SHOPIFY_ADMIN_TOKEN. Inventario completo en 14-secrets. - Observabilidad fragmentada: el log de un run vive en (a) Supabase logs del Edge, (b)
cron.job_run_detailssi el disparo fue desde cron, (c) GHA workflow run, (d) row deimport_runs, (e)reports/run.logen Storage. No hay tracing unificado. Para depurar, suele bastar empezar porimport_runs(status + error_stage) y derivar a los otros logs. - No lockfile en npm — el repo ignora
package-lock.json(policy Shopify theme). El workflow usanpm install, nonpm ci, y sin caché de setup-node. Documentado en 12-github-repo. verify_jwt=trueensftp-sync: la URL pública del edge function rechaza requests sin JWT.pg_croninyecta la anon key viaprivate.invoke_edge_function(..., with_auth=true). Documentado en 11-supabase.
Cambios¶
- v1.0 (17-may-2026): cabecera de estado actualizada; el documento estaba completo pero figuraba como v0.1.
- v0.1 (15-may-2026): primera publicación.