docs-builder: resolve env-dependent config values (starting with storybook.registry)
Problem
docset.yml is a static, committed artifact, but some config values are inherently environment-dependent. The motivating case is storybook.registry: the URL of the embeddable-Storybook registry differs per environment, varying only in one ref segment:
| Environment |
Registry URL |
| local |
http://127.0.0.1:6007/storybook-docs/docs_registry.json |
| PR preview |
https://ci-artifacts.kibana.dev/storybooks/pr-<N>/storybook-docs/docs_registry.json |
| main / production |
https://ci-artifacts.kibana.dev/storybooks/main/storybook-docs/docs_registry.json |
docs-builder reads the committed value, but the actor that knows the ref (pr-<N> vs main) at render time is docs-builder/the docs-preview service — not the author at commit time. Today this forces contributors to hand-edit docset.yml per environment and "revert before merge," which is error-prone. (Kibana ref: the preview host is literally kibana_bk_<PR#>.docs-preview.app.elstc.co, so the ref is known there.)
Proposed capability
Support environment-variable interpolation with a default for config values, at minimum storybook.registry:
storybook:
registry: ${KIBANA_STORYBOOK_REGISTRY:-https://ci-artifacts.kibana.dev/storybooks/main/storybook-docs/docs_registry.json}
Semantics (shell-style, familiar from Docker Compose / Elastic Agent):
${VAR} → value of VAR, or empty/error if unset (see edge cases).
${VAR:-default} → VAR if set and non-empty, else default.
- The committed value becomes identical across all environments; the environment supplies
VAR.
Responsibilities after this lands:
- main / production assembly:
VAR unset → resolves to the committed default (main). ✓
- PR preview: the docs-preview service exports
KIBANA_STORYBOOK_REGISTRY=…/pr-<N>/… (it already knows the PR). ✓
- local: contributor exports
KIBANA_STORYBOOK_REGISTRY=http://127.0.0.1:6007/storybook-docs/docs_registry.json before docs-builder serve. ✓
⚠️ Security constraint (shapes the design)
docs-builder builds untrusted PR branches. Naive ${VAR} interpolation over the full process environment is an exfiltration vector — a malicious PR could write ${AWS_SECRET_ACCESS_KEY} into docset.yml (or a substituted field that ends up in rendered output/logs) and leak CI secrets.
Therefore interpolation must be restricted to an allow-list, not arbitrary env. Options:
- A registered set of known-safe variable names (e.g. only vars the build harness explicitly opts in).
- A reserved namespace, e.g.
${docs.env.KIBANA_STORYBOOK_REGISTRY}, resolved only from an allow-listed mapping the harness populates — never from raw process.env.
Pick whichever fits docs-builder's config model; the non-negotiable is no unrestricted env access.
Fallback / ephemeral-registry mode
Two realities make a single hard URL insufficient for PR previews:
- Timing race: Kibana's storybook build+upload is a separate CI step; the
pr-<N> registry may not exist yet (or at all, for PRs that touch no stories) when the preview renders.
- Integrity pinning: the registry is published with
integrity: sha256-…. That's right for the reproducible main registry but incompatible with an ephemeral per-PR URL whose hash changes every build.
So the resolver should:
- Degrade gracefully: if the resolved registry is unreachable/404, fall back to a default (main) rather than hard-failing the embed. Consider supporting an explicit pair, e.g.
registry + fallback, or document that an unreachable registry → fall back to the committed default.
- Allow "no integrity" for the dynamic case: integrity may be pinned for the main/assembled site but must be optional/absent for ephemeral PR registries.
Scope / non-goals
- In scope: generic env interpolation w/ default + allow-list; applied at least to
storybook.registry; graceful fallback + optional integrity.
- Non-goal — do NOT bake repo conventions into docs-builder. docs-builder should not know how to construct a Kibana
pr-<N> URL. The pr-<N> scheme and bucket layout stay in Kibana/its CI and the docs-preview injection layer; docs-builder only provides the substitution primitive. (Keeps it reusable across repos.)
Acceptance criteria / test cases
Consumer-side migration (Kibana, separate PR)
docs/docset.yml → the interpolated form above (single value, no per-env edits).
- docs-preview injection: export
KIBANA_STORYBOOK_REGISTRY=…/pr-<N>/… for PR previews (derived from the PR number the preview already has).
scripts/storybook_docs --serve already prints the local registry URL; update it to print the KIBANA_STORYBOOK_REGISTRY=… docs-builder serve form.
- Removes the need for the "TEMPORARY / revert before merge" override and any commit-time guard.
docs-builder: resolve env-dependent config values (starting with
storybook.registry)Problem
docset.ymlis a static, committed artifact, but some config values are inherently environment-dependent. The motivating case isstorybook.registry: the URL of the embeddable-Storybook registry differs per environment, varying only in one ref segment:http://127.0.0.1:6007/storybook-docs/docs_registry.jsonhttps://ci-artifacts.kibana.dev/storybooks/pr-<N>/storybook-docs/docs_registry.jsonhttps://ci-artifacts.kibana.dev/storybooks/main/storybook-docs/docs_registry.jsondocs-builderreads the committed value, but the actor that knows the ref (pr-<N>vsmain) at render time is docs-builder/the docs-preview service — not the author at commit time. Today this forces contributors to hand-editdocset.ymlper environment and "revert before merge," which is error-prone. (Kibana ref: the preview host is literallykibana_bk_<PR#>.docs-preview.app.elstc.co, so the ref is known there.)Proposed capability
Support environment-variable interpolation with a default for config values, at minimum
storybook.registry:Semantics (shell-style, familiar from Docker Compose / Elastic Agent):
${VAR}→ value ofVAR, or empty/error if unset (see edge cases).${VAR:-default}→VARif set and non-empty, elsedefault.VAR.Responsibilities after this lands:
VARunset → resolves to the committed default (main). ✓KIBANA_STORYBOOK_REGISTRY=…/pr-<N>/…(it already knows the PR). ✓KIBANA_STORYBOOK_REGISTRY=http://127.0.0.1:6007/storybook-docs/docs_registry.jsonbeforedocs-builder serve. ✓docs-builder builds untrusted PR branches. Naive
${VAR}interpolation over the full process environment is an exfiltration vector — a malicious PR could write${AWS_SECRET_ACCESS_KEY}intodocset.yml(or a substituted field that ends up in rendered output/logs) and leak CI secrets.Therefore interpolation must be restricted to an allow-list, not arbitrary env. Options:
${docs.env.KIBANA_STORYBOOK_REGISTRY}, resolved only from an allow-listed mapping the harness populates — never from rawprocess.env.Pick whichever fits docs-builder's config model; the non-negotiable is no unrestricted env access.
Fallback / ephemeral-registry mode
Two realities make a single hard URL insufficient for PR previews:
pr-<N>registry may not exist yet (or at all, for PRs that touch no stories) when the preview renders.integrity: sha256-…. That's right for the reproducible main registry but incompatible with an ephemeral per-PR URL whose hash changes every build.So the resolver should:
registry+fallback, or document that an unreachable registry → fall back to the committed default.Scope / non-goals
storybook.registry; graceful fallback + optional integrity.pr-<N>URL. Thepr-<N>scheme and bucket layout stay in Kibana/its CI and the docs-preview injection layer; docs-builder only provides the substitution primitive. (Keeps it reusable across repos.)Acceptance criteria / test cases
${VAR:-default}resolves todefaultwhenVARis unset, toVARwhen set and non-empty.${VAR}with an unset, non-allow-listed name does not read process env (security test:${SECRET}is not interpolated/leaked).storybook.registrythat 404s falls back to the default and the build does not hard-fail; missing embeds degrade visibly but don't break the page.docs-builder servelocally picks upKIBANA_STORYBOOK_REGISTRYfrom the shell.Consumer-side migration (Kibana, separate PR)
docs/docset.yml→ the interpolated form above (single value, no per-env edits).KIBANA_STORYBOOK_REGISTRY=…/pr-<N>/…for PR previews (derived from the PR number the preview already has).scripts/storybook_docs --servealready prints the local registry URL; update it to print theKIBANA_STORYBOOK_REGISTRY=… docs-builder serveform.