Skip to content

[feature] Resolve env-dependent config values (starting with storybook.registry) #3485

@clintandrewhall

Description

@clintandrewhall

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:

  1. A registered set of known-safe variable names (e.g. only vars the build harness explicitly opts in).
  2. 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

  • ${VAR:-default} resolves to default when VAR is unset, to VAR when set and non-empty.
  • ${VAR} with an unset, non-allow-listed name does not read process env (security test: ${SECRET} is not interpolated/leaked).
  • Only allow-listed variables are interpolated; everything else is left literal or errors clearly.
  • A resolved storybook.registry that 404s falls back to the default and the build does not hard-fail; missing embeds degrade visibly but don't break the page.
  • Integrity is honored when present and skipped when absent (ephemeral mode).
  • docs-builder serve locally picks up KIBANA_STORYBOOK_REGISTRY from the shell.
  • main build with no env var produces the committed default URL byte-for-byte.

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions