ADR-015: Combined GitLab Pages Documentation Site for Mono-Repos¶
| Field | Value |
|---|---|
| Date | 2026-07-28 |
| Status | Accepted |
| Deciders | Holger Zahnleiter |
| Supersedes | ADR-012 (Pages-aggregation gap only) |
| Superseded by | ADR-016 (gating decision only) — rest still in force |
Context and Problem Statement¶
ADR-012 deliberately deferred combined GitLab Pages documentation for mono-repos: GitLab allows
only one Pages site per project, so every mono-repo child's own pages job is suppressed entirely
today ({{if not .IsMonoRepoChild}} in all 9 per-language CI templates), and no artifact's docs
are published at all. Reviewing the demo mono-repo surfaced the natural follow-up question: can
several artifacts' independently generated mkdocs.yml/specs/ trees be combined into one
navigable site, with each artifact appearing as a real top-level nav tab (not just a link on a
landing page)?
$CI_PROJECT_DIR is already the mono-repo root for every job, including triggered child
pipelines (ADR-012's own correction) — so a combined build does not need GitLab's cross-pipeline
artifact-fetching mechanics at all. A single job in the root .gitlab-ci.yml, which today only
contains per-artifact trigger jobs, can build directly from the full checkout: the root's own
ADR-013 specs scaffold plus every artifact's own, completely unmodified mkdocs.yml.
Decision Drivers¶
- Real top-level nav tabs per artifact (not a links-only landing page) were the user's explicit requirement, not just "some way to reach every artifact's docs from one place."
- No per-artifact
mkdocs.ymlshould need modification — each keeps generating exactly what it generates today, standalone or combined. - Minimize the blast radius of a first attempt: prove the approach works with a local build before making any permanent, harder-to-reverse change (a new baked Docker image version).
- Stay honest about where this deliberately diverges from existing conventions elsewhere in this generator, rather than let the divergence go undocumented.
Considered Options¶
mkdocs-monorepo-plugin(!includenav directive) — rootmkdocs.ymlincludes each artifact's ownmkdocs.ymlunmodified; one combinedmkdocs buildproduces real top-level tabs via mkdocs-material'snavigation.tabsfeature. (chosen)- Root landing page with links — each artifact keeps its own independent, separately-built site; a root page links to each. No new plugin, no coupling between artifacts' builds, but no real tab-level navigation — an extra click, and no unified nav tree.
For the plugin's own installation:
- Install
mkdocs-monorepo-pluginat CI-runtime (pip install), using the existing, unmodifiedmkdocs:1.0.0image (with one small, independent entrypoint fix — see below). (chosen for this pass) - Bake the plugin permanently into the shared
mkdocsDocker image (build-imagesrepo).
Decision Outcome¶
Chosen: Option 1 (mkdocs-monorepo-plugin) + Option 3 (install at CI-runtime, prove it first).
Root mkdocs.yml (new, mono-repo-only)¶
repo init now writes a root mkdocs.yml (WriteMonoRepoRootMkdocs, mirroring the
WriteMonoRepoRootSpecs/writeMonoRepoRootGitlabCI root-file precedent from ADR-012/013). It
combines the root's own 3 ADR-013 files via a static nav: (not gen-files/literate-nav — the
two mechanisms don't compose: !include is a YAML directive resolved inside nav: itself, while
literate-nav reads a separate SUMMARY.md; there is no way to emit a !include line from
literate-nav's Markdown-bullet-list source) with one !include ./<artifact>/mkdocs.yml line per
manifest entry, verbatim, unmodified:
plugins:
- search
- monorepo
- mermaid2:
javascript: assets/js/mermaid.min.js
nav:
- Overview: index.md
- System-Wide ADRs: architecture/ADRs/README.md
- System-Wide Business Rules: domain/business_rules.md
- Payment: '!include ./payment/mkdocs.yml'
- Car Rental: '!include ./car-rental/mkdocs.yml'
Validated locally end-to-end (a scratch root mkdocs.yml against the demo repo's 6 already-real
artifacts, before writing any generator code): mkdocs-monorepo-plugin==1.1.2 against mkdocs
1.6.1 builds cleanly, correctly walks into each included artifact's own docs_dir: specs and its
own gen-files/literate-nav-generated pages, and produces real top-level tabs per artifact.
Root pages job (new, in root .gitlab-ci.yml)¶
The root .gitlab-ci.yml, previously pure trigger-dispatcher with no stages: block at all,
gains an explicit stages: [trigger, docs] (every existing trigger job gets stage: trigger to
avoid breaking, since they previously landed in GitLab's implicit default test stage) plus:
pages:
stage: docs
needs: [] # runs immediately; depends on no trigger job's child pipeline result
image:
name: registry.gitlab.com/hzahnlei/build-images/mkdocs:1.1.0
entrypoint: [""]
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
script:
- pip install --no-cache-dir mkdocs-monorepo-plugin==1.1.2
- mkdocs build --site-dir public
artifacts:
paths:
- public
expire_in: never
needs: [] is necessary, not optional: GitLab runs stages sequentially, and every trigger job
uses strategy: depend — without needs: [], the new docs stage would wait for every triggered
child pipeline in that push to fully complete before starting, despite depending on none of their
output.
Independent entrypoint fix (build-images repo)¶
Each included artifact's own mermaid2 plugin config (javascript: assets/js/mermaid.min.js,
relative to its own docs_dir) needs a real file at <artifact>/specs/assets/js/mermaid.min.js.
The shared mkdocs image's entrypoint previously injected mermaid.min.js only into the checkout
root's specs/assets/js/. Rather than add a CI-script copy-loop (which would violate this
generator's own general_coding.md guideline — "check whether the tool can read directly from
the original location instead of copying a file to make it findable"), entrypoint-mkdocs.sh was
extended to loop over specs */specs instead of just specs — backward-compatible (a standalone
repo has no */specs, so the glob matches nothing extra). Released as mkdocs:1.1.0. No other
per-artifact template or image change was needed for this: standalone repos keep working with
either tag, and every mono-repo child's own pages job stays suppressed entirely (ADR-012)
regardless, so no per-language template needs the new tag.
Deliberate deviations, named explicitly¶
- Temporary exception to
tool_chain.md's own "no installation during CI runs" / "self-contained, no internet access needed" guideline. The newpagesjob'spip installviolates what this generator prescribes to every project it generates. Accepted deliberately and temporarily: prove the plugin combination actually works (it is officially beta — "may not yet be fully compatible with other Mkdocs configuration," per its own documentation, with a changelog history of breaking against newer mkdocs-core releases) before committing to a permanent, harder-to-reverse Docker image change. Follow-up: bakemkdocs-monorepo-plugininto the image once this has run in production for a while, removing the exception. - Deviation from ADR-003 (publish docs only on
vX.Y.Ztags): this combined site is branch- gated, not tag-gated, because it is not a versioned release artifact of any single service — it's a living, always-current combined view. Named explicitly rather than silently picking a different trigger. Superseded by ADR-016: this deviation was reverted. The site is now built on every docs change but published only on an artifact release tag, and it carries a build date plus a table of the artifacts' released versions instead of a version of its own. - Known cosmetic quirk, not a defect:
mkdocs-monorepo-pluginderives each included artifact's URL segment from that artifact's ownsite_name(verbatim if it matches^[a-zA-Z0-9_.\-/]+$, slugified otherwise) — not from the!includepath or the root nav's label. A single-word project name (e.g. "Payment") keeps its original casing (/Payment/...); a multi-word one (e.g. "Car Rental") gets slugified to lowercase-hyphenated (/car-rental/...). This is the plugin's own behavior, out of this generator's control without changing every per-artifactsite_name— accepted as a minor inconsistency, not worth a workaround. - Pre-existing relative-link breakage, not introduced by this ADR: several
specs/files already contain relative Markdown links between sibling artifacts (e.g.../../../car-rental/specs/domain/business_rules.md) that predate any combined build and don't resolve to the new combined URL structure.mkdocs buildreports these as warnings, not failures; fixing them is a separate, pre-existing cleanup, out of scope here.
Positive Consequences¶
- Real, working combined documentation site with genuine top-level nav tabs per artifact, matching the explicit requirement — validated with an actual local build, not just template review.
- No per-artifact
mkdocs.ymlis modified — the same file works standalone or combined, unchanged. - The image change (entrypoint fix) is small, independent of the (deferred) plugin bake-in, and backward-compatible for every existing consumer of the image.
Negative Consequences / Risks¶
- The whole combined build is now a single process: a broken
mkdocs.yml/specs/in any one artifact could break the combined site's build for every artifact, a coupling that didn't exist before (each mono-repo child's own docs job was fully isolated, if suppressed). mkdocs-monorepo-pluginis beta software with a history of breaking against mkdocs-core upgrades; pinning helps, but futuremkdocs/mkdocs-materialupgrades in the shared image need to re-verify compatibility, not just bump the version blindly.- The
pip install-at-runtime step depends on outbound internet access during that one CI job, unlike every other job in this generator's output.
Pros and Cons of the Options¶
Option 1 — mkdocs-monorepo-plugin (chosen)¶
- Pro: real top-level tabs, matches the explicit requirement.
- Pro: no per-artifact
mkdocs.ymlmodification. - Con: beta plugin, couples all artifacts' docs builds into one process.
Option 2 — Root landing page with links¶
- Pro: zero new dependency, builds stay fully isolated per artifact.
- Con: no real tab-level navigation — rejected because it doesn't meet the stated requirement.
Option 3 — Install plugin at CI-runtime (chosen for this pass)¶
- Pro: validates the whole approach before any permanent image change; image stays untouched except for the small, independent, backward-compatible entrypoint fix.
- Con: temporary, explicit violation of this generator's own
tool_chain.mdguideline; CI-runtime internet dependency.
Option 4 — Bake into the image now¶
- Pro: no guideline exception, no CI-runtime internet dependency.
- Con: commits to a permanent image change before the plugin combination has been proven to actually work end-to-end — rejected for this first pass; planned as the follow-up once proven.
Links¶
specs/architecture/ADRs/ADR-012-mono_repo_ci_composition.md— the Pages-aggregation gap this ADR closes; "Superseded by" amended to reference this ADR for that gap specifically.specs/architecture/ADRs/ADR-013-mono_repo_root_specs_and_contracts.md— the root-file precedent (WriteMonoRepoRootSpecs) this decision'sWriteMonoRepoRootMkdocsmirrors.specs/architecture/ADRs/ADR-003-gitlab_pages_release_docs.md— the tag-gated Pages convention this decision deliberately deviates from for the combined site specifically.specs/guidelines/tool_chain.md.tmpl,specs/guidelines/general_coding.md.tmpl— the guidelines this decision names an explicit, temporary exception to (CI-runtime install) and avoids violating (mermaid.min.js injection, fixed at the image's entrypoint instead of a CI-script copy-loop).cmd/mono_repo_root_ci.go—writeMonoRepoRootGitlabCI's newpagesjob.internal/templating/general/mono_repo_root_mkdocs.go,internal/templating/general/files/mono_repo_root/mkdocs.yml.tmpl— the new rootmkdocs.yml./Users/holgerzahnleiter/projekte/eigene/build-images/image_definitions/entrypoint-mkdocs.sh— the independent entrypoint fix, released asmkdocs:1.1.0.