diff --git a/.dockerignore b/.dockerignore index 44021c6..f2f978d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,4 +11,8 @@ uploads .env.* !.env.example test -docs + +# docs/ is NOT ignored: the board serves it at /docs, rendering the same +# markdown files the repository keeps. Excluding it builds an image whose +# documentation pages all 404 while the index quietly renders its "deployed +# without its docs directory" fallback — a failure that looks like nothing. diff --git a/apps/server/src/routes/docs.ts b/apps/server/src/routes/docs.ts index 2871fee..e75f279 100644 --- a/apps/server/src/routes/docs.ts +++ b/apps/server/src/routes/docs.ts @@ -1,4 +1,4 @@ -import { readFileSync, statSync } from 'node:fs'; +import { existsSync, readFileSync, statSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Hono } from 'hono'; @@ -180,6 +180,21 @@ function load(doc: Doc): Rendered | null { export function docsRoutes(services: Services) { const app = new Hono(); + /* + * Say so at boot if the documents are not in the image. + * + * They ship as files, so a build that excludes them — one line in + * .dockerignore did exactly this — leaves every /docs page answering 404 + * while the index renders its "no documentation" fallback perfectly happily. + * That is a failure with no symptom anybody would notice from the outside, + * which is precisely the kind worth a line in the log. + */ + if (!existsSync(DOCS_DIR)) { + console.warn( + `[tsbb] no docs directory at ${DOCS_DIR} — /docs will be empty. Is it excluded from the build?`, + ); + } + app.get('/docs', async (c) => { const entries = DOCS.map((doc) => ({ doc, rendered: load(doc) })).filter( (entry): entry is { doc: Doc; rendered: Rendered } => entry.rendered !== null, diff --git a/test/docs.test.ts b/test/docs.test.ts index 0a5e1a5..ae4c857 100644 --- a/test/docs.test.ts +++ b/test/docs.test.ts @@ -113,6 +113,31 @@ describe('the documentation pages', () => { assert.equal(missing.status, 404); }); + /* + * These pages passed every test above and still 404'd in production, because + * .dockerignore excluded docs/ and the deployed image had no files to render. + * Nothing that boots the app in a checkout can catch that — the guard has to + * be on the thing that was wrong, which is the build configuration. + */ + it('ships the documents in the image', async () => { + const { readFileSync } = await import('node:fs'); + const { dirname, join } = await import('node:path'); + const { fileURLToPath } = await import('node:url'); + const root = join(dirname(fileURLToPath(import.meta.url)), '..'); + + const ignored = readFileSync(join(root, '.dockerignore'), 'utf8') + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')); + + for (const pattern of ['docs', 'docs/', '/docs', './docs']) { + assert.ok( + !ignored.includes(pattern), + `.dockerignore excludes ${pattern}, so the built image would serve no documentation`, + ); + } + }); + it('points at the docs from the API index, for a human who found the JSON', async () => { const response = await app.fetch(new Request('http://localhost:3993/api/v1')); const index = (await response.json()) as { docs: string };