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
templates/ is seven .md files at the repo root — digest, escalation, invite, sla-breach, ticket-created, ticket-resolved, welcome.
.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.
- 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.
findTemplatesDir() (packages/outpost/shared/src/templates/loader.ts:50-68) walks four candidate paths, finds none, and falls back to join(process.cwd(), 'templates').
loadFromFilesystem (:70-82) — readFileSync throws, the catch returns null.
sendEmail (packages/outpost/shared/src/email/sender.ts:166-168) returns { success: false, error: 'Template "invite" not found', method: 'console' }.
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
Every email template is absent from the production web image, so every send fails and returns
emailSent: false. This is onmaintoday; 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
templates/is seven.mdfiles at the repo root —digest,escalation,invite,sla-breach,ticket-created,ticket-resolved,welcome..dockerignore:7is*.md, and:8re-includes only!README.md. The templates never enter the build context, so the pruner'sCOPY . .cannot pick them up.templates/—grep -rn "templates" --include="Dockerfile*" .returns nothing.apps/web/Dockerfile's runner stage copies.next/standalone,.next/static,public, the Prisma artifacts andstart.sh.findTemplatesDir()(packages/outpost/shared/src/templates/loader.ts:50-68) walks four candidate paths, finds none, and falls back tojoin(process.cwd(), 'templates').loadFromFilesystem(:70-82) —readFileSyncthrows, thecatchreturnsnull.sendEmail(packages/outpost/shared/src/email/sender.ts:166-168) returns{ success: false, error: 'Template "invite" not found', method: 'console' }.apps/web/src/app/api/team/invite/route.ts:92-95turns that into aconsole.warnandNextResponse.json({ ...responseData, emailSent: false }).Impact
sendEmailhas 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 theinviteUrlby 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.warnplus a JSON field. Whether the UI surfacesemailSent: falseis worth checking as part of this.Why it has stayed hidden
The 35 template tests run from
apps/web, wherejoin(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:
…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-computedreadFileSyncpath, so I do not think it saves this — butdocker buildonapps/webthenls templatesin the runner settles it in a minute, and I would rather be corrected than have this taken on my reading.Related