mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(docker): ship Dashboard Docs markdown in the container image (#2348)
`.dockerignore` excluded everything under `docs/` except `openapi.yaml`, which broke the in-product Docs viewer at `/docs/*` with "ENOENT: no such file or directory, open '/app/docs/SETUP_GUIDE.md'" for every help page. The previous block-list was meant to keep the image small but it hid the ~5 MB English markdown tree that the viewer actually reads. Replace the block with a targeted exclude of the heavy assets that account for ~45 MB of the original ~50 MB: - docs/i18n/** (translated copies — viewer falls back to English) - docs/screenshots/** - docs/diagrams/exported/** plus raster/SVG variants Note: Go filepath.Match (Docker's matcher) treats `*` as not crossing `/`, so the existing `*.md` rule still excludes only root-level markdown — nested `docs/**/*.md` is implicitly kept without a re-include rule that would have brought i18n back. Added a regression test that parses .dockerignore and asserts the critical English docs survive the filter while the heavy paths still do not.
This commit is contained in:
@@ -37,10 +37,26 @@ test-results
|
||||
playwright-report
|
||||
blob-report
|
||||
|
||||
# Documentation (not needed in container)
|
||||
# Keep the docs directory itself in context so openapi.yaml can be re-included.
|
||||
docs/*
|
||||
!docs/openapi.yaml
|
||||
# Documentation
|
||||
# Issue #2348: The Dashboard Docs viewer reads markdown from `/app/docs` at
|
||||
# runtime. The previous `docs/*` block hid every file except openapi.yaml,
|
||||
# so the in-product help screen failed with ENOENT for every page.
|
||||
# We now keep the English markdown tree but drop the bulky assets
|
||||
# (translations, screenshots, raster diagrams) that account for ~45 MB
|
||||
# of the ~50 MB docs directory. The Docs viewer reads the default-locale
|
||||
# (English) sources at runtime, so translations are not required in the
|
||||
# container image.
|
||||
docs/i18n/**
|
||||
docs/screenshots/**
|
||||
docs/diagrams/exported/**
|
||||
docs/diagrams/**/*.png
|
||||
docs/diagrams/**/*.jpg
|
||||
docs/diagrams/**/*.jpeg
|
||||
docs/diagrams/**/*.gif
|
||||
docs/diagrams/**/*.webp
|
||||
docs/diagrams/**/*.svg
|
||||
# Note: `*.md` matches the root only (Go filepath.Match does not cross /),
|
||||
# so nested docs/**/*.md is implicitly kept without a re-include rule.
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
|
||||
153
tests/unit/dockerignore-docs-coverage.test.ts
Normal file
153
tests/unit/dockerignore-docs-coverage.test.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Issue #2348 — Regression guard for the Docker image documentation bundle.
|
||||
*
|
||||
* The Dashboard Docs viewer at `src/app/docs/[slug]/page.tsx` reads markdown
|
||||
* from `process.cwd()/docs/<file>` at runtime. The previous `.dockerignore`
|
||||
* shipped only `docs/openapi.yaml` to the container, so every help screen
|
||||
* threw ENOENT.
|
||||
*
|
||||
* This test parses `.dockerignore`, applies it against the working tree,
|
||||
* and asserts that the critical English markdown files are still in the
|
||||
* Docker build context.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = path.resolve(__dirname, "../..");
|
||||
const DOCKERIGNORE = path.resolve(REPO_ROOT, ".dockerignore");
|
||||
|
||||
// Subset of the docs viewer's catalog that MUST survive the docker filter.
|
||||
// Sourced from src/app/docs/lib/docs-auto-generated.ts.
|
||||
const REQUIRED_DOCS = [
|
||||
"docs/README.md",
|
||||
"docs/PROVIDERS.md",
|
||||
"docs/AUTO-COMBO.md",
|
||||
"docs/guides/SETUP_GUIDE.md",
|
||||
"docs/guides/TROUBLESHOOTING.md",
|
||||
"docs/reference/API_REFERENCE.md",
|
||||
"docs/reference/PROVIDER_REFERENCE.md",
|
||||
"docs/reference/ENVIRONMENT.md",
|
||||
];
|
||||
|
||||
// Compile .dockerignore patterns into a simple matcher.
|
||||
// We only need to support the directives we actually use: glob `**`, plain
|
||||
// path prefixes, and negations starting with `!`.
|
||||
function parseDockerignore(text: string): { excludes: string[]; includes: string[] } {
|
||||
const excludes: string[] = [];
|
||||
const includes: string[] = [];
|
||||
for (const raw of text.split(/\r?\n/)) {
|
||||
const line = raw.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
if (line.startsWith("!")) {
|
||||
includes.push(line.slice(1));
|
||||
} else {
|
||||
excludes.push(line);
|
||||
}
|
||||
}
|
||||
return { excludes, includes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Match a Docker glob pattern against a file path without building a dynamic
|
||||
* RegExp (avoid ReDoS risk on patterns sourced from .dockerignore). We walk
|
||||
* the pattern token-by-token. Supported syntax (the subset our .dockerignore
|
||||
* actually uses): literal segments, `*` (no slash), `**` (any depth incl. 0),
|
||||
* and trailing `**`.
|
||||
*/
|
||||
function patternMatches(pattern: string, file: string): boolean {
|
||||
const pSegs = pattern.split("/");
|
||||
const fSegs = file.split("/");
|
||||
return matchSegments(pSegs, 0, fSegs, 0);
|
||||
}
|
||||
|
||||
function matchSegments(p: string[], pi: number, f: string[], fi: number): boolean {
|
||||
while (pi < p.length) {
|
||||
const seg = p[pi];
|
||||
if (seg === "**") {
|
||||
// Try consuming 0..N file segments.
|
||||
if (pi === p.length - 1) return true; // trailing ** consumes everything
|
||||
for (let k = fi; k <= f.length; k++) {
|
||||
if (matchSegments(p, pi + 1, f, k)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (fi >= f.length) return false;
|
||||
if (!segmentMatches(seg, f[fi])) return false;
|
||||
pi++;
|
||||
fi++;
|
||||
}
|
||||
return fi === f.length;
|
||||
}
|
||||
|
||||
function segmentMatches(pattern: string, segment: string): boolean {
|
||||
if (pattern === "*") return true;
|
||||
if (!pattern.includes("*")) return pattern === segment;
|
||||
// Single-segment glob with one or more `*` wildcards. Walk literal chunks.
|
||||
const parts = pattern.split("*");
|
||||
let cursor = 0;
|
||||
// Anchor first chunk to start.
|
||||
const first = parts[0];
|
||||
if (first && !segment.startsWith(first)) return false;
|
||||
cursor = first.length;
|
||||
// Anchor last chunk to end.
|
||||
const last = parts[parts.length - 1];
|
||||
if (last && !segment.endsWith(last)) return false;
|
||||
// Each middle chunk must appear in order between cursor and end-last.
|
||||
const endLimit = segment.length - last.length;
|
||||
for (let i = 1; i < parts.length - 1; i++) {
|
||||
const idx = segment.indexOf(parts[i], cursor);
|
||||
if (idx === -1 || idx + parts[i].length > endLimit) return false;
|
||||
cursor = idx + parts[i].length;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isIgnored(file: string, parsed: { excludes: string[]; includes: string[] }): boolean {
|
||||
let ignored = false;
|
||||
for (const ex of parsed.excludes) if (patternMatches(ex, file)) ignored = true;
|
||||
if (ignored) {
|
||||
for (const inc of parsed.includes) if (patternMatches(inc, file)) ignored = false;
|
||||
}
|
||||
return ignored;
|
||||
}
|
||||
|
||||
test("#2348 .dockerignore keeps every doc the in-product viewer needs", () => {
|
||||
const parsed = parseDockerignore(fs.readFileSync(DOCKERIGNORE, "utf8"));
|
||||
const missing: string[] = [];
|
||||
const ignored: string[] = [];
|
||||
|
||||
for (const docPath of REQUIRED_DOCS) {
|
||||
const absPath = path.resolve(REPO_ROOT, docPath);
|
||||
if (!fs.existsSync(absPath)) {
|
||||
missing.push(docPath);
|
||||
continue;
|
||||
}
|
||||
if (isIgnored(docPath, parsed)) {
|
||||
ignored.push(docPath);
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepEqual(missing, [], `Required docs missing from repo: ${missing.join(", ")}`);
|
||||
assert.deepEqual(
|
||||
ignored,
|
||||
[],
|
||||
`Files excluded from Docker context — Dashboard Docs viewer will 404 on these:\n ${ignored.join("\n ")}`
|
||||
);
|
||||
});
|
||||
|
||||
test("#2348 .dockerignore still excludes the heavy i18n + screenshots dirs", () => {
|
||||
const parsed = parseDockerignore(fs.readFileSync(DOCKERIGNORE, "utf8"));
|
||||
// These should NOT make it into the container — they are 45+ MB combined
|
||||
// and the in-product viewer reads only the English originals at runtime.
|
||||
const HEAVY_PATHS = ["docs/i18n/pt-BR/docs/AUTO-COMBO.md", "docs/screenshots/dashboard.png"];
|
||||
for (const heavy of HEAVY_PATHS) {
|
||||
assert.ok(
|
||||
isIgnored(heavy, parsed),
|
||||
`${heavy} should be excluded from Docker context but is not — image size will balloon`
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user