diff --git a/.gitea/workflows/deploy-galactus.yml b/.gitea/workflows/deploy-galactus.yml index eedaaa9..8a1ed98 100644 --- a/.gitea/workflows/deploy-galactus.yml +++ b/.gitea/workflows/deploy-galactus.yml @@ -40,6 +40,18 @@ # SESSION_SECRET_GALACTUS 64-hex (openssl rand -hex 32) # MINIO_ROOT_USER / MINIO_ROOT_PASSWORD # MYSQL_PASSWORD / MYSQL_ROOT_PASSWORD +# Optional — outbound mail. Not needed to deploy; needed for +# /notificaciones to send anything at all (the image sets +# NODE_ENV=production, which disables MailService's stdout fallback, so +# a blank config fails every send loudly): +# SES_REGION e.g. us-west-2 +# SES_FROM a VERIFIED SES sending identity +# SES_FROM_NAME display name, optional +# SES_ACCESS_KEY / SES_SECRET_KEY +# SES_CONFIGURATION_SET optional, for bounce/complaint events +# NOTIFICATION_ADMIN_EMAILS comma-separated summary recipients +# These are NOT galactus-specific (no _GALACTUS suffix) — one SES identity +# serves every deployment. # - The runner (which lives on cubex) must be able to reach BOTH # galactus:9443 (Portainer) and galactus:3306 (MySQL, for migrate deploy). # If it cannot reach 3306, run the migration by hand from a host that can @@ -116,6 +128,13 @@ jobs: MINIO_ROOT_PASSWORD: ${{ secrets.MINIO_ROOT_PASSWORD }} MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }} + # Not required — the app boots fine without mail. Warned about below, + # because the failure mode is remote: everything looks healthy until + # someone clicks "Ejecutar" and every send fails. + SES_REGION: ${{ secrets.SES_REGION }} + SES_FROM: ${{ secrets.SES_FROM }} + SES_ACCESS_KEY: ${{ secrets.SES_ACCESS_KEY }} + SES_SECRET_KEY: ${{ secrets.SES_SECRET_KEY }} SCOPE: ${{ github.event.inputs.scope }} run: | REQUIRED="PORTAINER_URL_GALACTUS PORTAINER_API_KEY_GALACTUS @@ -140,6 +159,20 @@ jobs: fi echo "all required secrets present for scope=$SCOPE" + # Mail is optional to deploy but not optional to work. Say so loudly + # rather than letting /notificaciones fail one send at a time. + mail_missing="" + for name in SES_REGION SES_FROM SES_ACCESS_KEY SES_SECRET_KEY; do + eval "value=\${$name}" + [ -z "$value" ] && mail_missing="$mail_missing $name" + done + if [ -n "$mail_missing" ]; then + echo "::warning::outbound mail is NOT configured, missing:$mail_missing" + echo "::warning::the deploy will succeed, but every notification and" + echo "::warning::renewal aviso will fail with 'El envío de correo no" + echo "::warning::está configurado.' See docs/MASS_EMAIL_NOTIFICATIONS.md" + fi + # --- full only: database --------------------------------------------- - name: Deploy database stack if: ${{ github.event.inputs.scope == 'full' }} @@ -260,7 +293,14 @@ jobs: "OPS_DB_ADMIN_USER": "root", "OPS_DB_ADMIN_PASSWORD": "${{ secrets.MYSQL_ROOT_PASSWORD }}", "MINIO_ROOT_USER": "${{ secrets.MINIO_ROOT_USER }}", - "MINIO_ROOT_PASSWORD": "${{ secrets.MINIO_ROOT_PASSWORD }}" + "MINIO_ROOT_PASSWORD": "${{ secrets.MINIO_ROOT_PASSWORD }}", + "SES_REGION": "${{ secrets.SES_REGION }}", + "SES_FROM": "${{ secrets.SES_FROM }}", + "SES_FROM_NAME": "${{ secrets.SES_FROM_NAME }}", + "SES_ACCESS_KEY": "${{ secrets.SES_ACCESS_KEY }}", + "SES_SECRET_KEY": "${{ secrets.SES_SECRET_KEY }}", + "SES_CONFIGURATION_SET": "${{ secrets.SES_CONFIGURATION_SET }}", + "NOTIFICATION_ADMIN_EMAILS": "${{ secrets.NOTIFICATION_ADMIN_EMAILS }}" } # --- prove it ---------------------------------------------------------- diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 6280030..4889a02 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -267,7 +267,14 @@ jobs: "OPS_DB_ADMIN_USER": "root", "OPS_DB_ADMIN_PASSWORD": "${{ secrets.MYSQL_ROOT_PASSWORD }}", "MINIO_ROOT_USER": "${{ secrets.MINIO_ROOT_USER }}", - "MINIO_ROOT_PASSWORD": "${{ secrets.MINIO_ROOT_PASSWORD }}" + "MINIO_ROOT_PASSWORD": "${{ secrets.MINIO_ROOT_PASSWORD }}", + "SES_REGION": "${{ secrets.SES_REGION }}", + "SES_FROM": "${{ secrets.SES_FROM }}", + "SES_FROM_NAME": "${{ secrets.SES_FROM_NAME }}", + "SES_ACCESS_KEY": "${{ secrets.SES_ACCESS_KEY }}", + "SES_SECRET_KEY": "${{ secrets.SES_SECRET_KEY }}", + "SES_CONFIGURATION_SET": "${{ secrets.SES_CONFIGURATION_SET }}", + "NOTIFICATION_ADMIN_EMAILS": "${{ secrets.NOTIFICATION_ADMIN_EMAILS }}" } # --- prove it ---------------------------------------------------------- diff --git a/deploy/galactus/jorgecuadros-app.compose.yml b/deploy/galactus/jorgecuadros-app.compose.yml index 321ee37..92dafaa 100644 --- a/deploy/galactus/jorgecuadros-app.compose.yml +++ b/deploy/galactus/jorgecuadros-app.compose.yml @@ -73,13 +73,16 @@ services: S3_BUCKET: ${S3_BUCKET:-jorgecuadros-documents} MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER must be set} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD must be set} - # Outbound mail (SES). Runtime config, never baked into the image and - # never a CI secret — the build does not send mail, this container does. + # Outbound mail (SES). Runtime config — read at container boot, never + # baked into the image; the build does not send mail, this container + # does. Values arrive the same way DATABASE_URL does: as Gitea repo + # secrets, injected into this stack's env by the `env_data` block of + # .gitea/workflows/deploy-galactus.yml. + # # The image sets NODE_ENV=production, which disables MailService's # stdout dev fallback: leave these blank and every notification and # renewal aviso fails with "El envío de correo no está configurado." - # rather than silently going nowhere. Values live in this stack's env - # file on galactus (deploy/.env.prod), same as DATABASE_URL. + # rather than silently going nowhere. SES_REGION: ${SES_REGION:-} SES_FROM: ${SES_FROM:-} SES_FROM_NAME: ${SES_FROM_NAME:-} diff --git a/deploy/jorgecuadros-app.env.example b/deploy/jorgecuadros-app.env.example index 5bb2235..427eae3 100644 --- a/deploy/jorgecuadros-app.env.example +++ b/deploy/jorgecuadros-app.env.example @@ -34,9 +34,13 @@ MINIO_ROOT_USER=jc_minio MINIO_ROOT_PASSWORD=CHANGE_ME # --- Outbound mail (Amazon SES) ---------------------------------------------- -# Belongs HERE, in the stack's env file on the host — not in Gitea Actions -# secrets. The build never sends mail; the running container does, and it reads -# these at boot (apps/api/src/mail/mail.service.ts). +# NOTE: for the Portainer-deployed stacks these do NOT come from a file on the +# host — the deploy workflows build the stack env from Gitea repo secrets (see +# the `env_data` blocks in .gitea/workflows/deploy*.yml). This file documents +# the full variable set and is what you fill in for a hand-run stack. +# +# Either way they are RUNTIME config, read at container boot +# (apps/api/src/mail/mail.service.ts) — never baked into the image. # # The production image sets NODE_ENV=production, which turns OFF the stdout dev # fallback. Leaving these blank does not silently swallow mail — every send diff --git a/docs/MASS_EMAIL_NOTIFICATIONS.md b/docs/MASS_EMAIL_NOTIFICATIONS.md index 9a2baf6..3eebddd 100644 --- a/docs/MASS_EMAIL_NOTIFICATIONS.md +++ b/docs/MASS_EMAIL_NOTIFICATIONS.md @@ -138,11 +138,17 @@ Without SES_* the API still boots and `MailService` falls back to stdout in dev (`NODE_ENV !== "production"`). In production every send throws `ServiceUnavailableException` and the row is recorded as `FAILED`. -These are **runtime** config, set in the deployed stack's env file on the -host (`deploy/jorgecuadros-app.env.example` documents the full set) — not -Gitea Actions secrets. The build never sends mail; only the running -container does, and the production image sets `NODE_ENV=production`, so a -blank SES config fails loudly rather than falling back to stdout. +These are **runtime** config — read at container boot, never baked into the +image. For the Portainer deployments they are set as **Gitea repo secrets** +and injected into the stack env by the `env_data` block of +`.gitea/workflows/deploy-galactus.yml` (and `deploy.yml`), exactly like +`DATABASE_URL` and `SESSION_SECRET`. Unlike most secrets there they carry no +`_GALACTUS` suffix: one SES identity serves every deployment. + +They are optional to *deploy* — the preflight only warns — but the +production image sets `NODE_ENV=production`, which disables the stdout dev +fallback, so a blank SES config makes every send fail loudly rather than +quietly going nowhere. ## UI