Skip to content

templates/ never reaches the production image, so every email send fails with emailSent: false #253

Description

@jerelvelarde

Every email template is absent from the production web image, so every send fails and returns emailSent: false. This is on main today; no PR introduces it and no open issue covers it.

Raised by @NathanTarbert on #248 as "worth confirming against a built image". I confirmed it against the tree.

The chain

  1. templates/ is seven .md files at the repo root — digest, escalation, invite, sla-breach, ticket-created, ticket-resolved, welcome.
  2. .dockerignore:7 is *.md, and :8 re-includes only !README.md. The templates never enter the build context, so the pruner's COPY . . cannot pick them up.
  3. No Dockerfile in the repo copies templates/grep -rn "templates" --include="Dockerfile*" . returns nothing. apps/web/Dockerfile's runner stage copies .next/standalone, .next/static, public, the Prisma artifacts and start.sh.
  4. findTemplatesDir() (packages/outpost/shared/src/templates/loader.ts:50-68) walks four candidate paths, finds none, and falls back to join(process.cwd(), 'templates').
  5. loadFromFilesystem (:70-82) — readFileSync throws, the catch returns null.
  6. sendEmail (packages/outpost/shared/src/email/sender.ts:166-168) returns { success: false, error: 'Template "invite" not found', method: 'console' }.
  7. apps/web/src/app/api/team/invite/route.ts:92-95 turns that into a console.warn and NextResponse.json({ ...responseData, emailSent: false }).

Impact

sendEmail has exactly two live call sites, both invites (api/team/invite/route.ts:72, api/team/invite/resend/route.ts:52). So in production, inviting a teammate creates the member row and sends nothing — the invitee has to be sent the inviteUrl by hand. The remaining six templates are unreachable for the same reason if anything starts using them.

It fails as a returned value rather than a throw, and the only signal is one console.warn plus a JSON field. Whether the UI surfaces emailSent: false is worth checking as part of this.

Why it has stayed hidden

The 35 template tests run from apps/web, where join(cwd, '..', '..', 'templates') resolves against the real repo root. They pass for a reason that does not exist inside the image.

Fix

Either un-ignore the directory and copy it into the runner stage:

# .dockerignore
*.md
!README.md
!templates/*.md
# apps/web/Dockerfile, runner stage
COPY --from=installer --chown=outpost:outpost /app/templates ./templates

…or stop loading them from disk at runtime — import them as modules so bundling carries them, which also removes findTemplatesDir()'s four-candidate CWD walk.

Whichever way, this wants a test that fails when the templates are not resolvable, rather than one that passes because it runs from a checkout.

Not verified

I read the Dockerfiles and .dockerignore, not a built image. Next's standalone output tracing will not pick up a runtime-computed readFileSync path, so I do not think it saves this — but docker build on apps/web then ls templates in the runner settles it in a minute, and I would rather be corrected than have this taken on my reading.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: infrastructureWorker, queue, CI, deploy, containers, observabilitybugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions