Dependency Management

How the resources/schemas/dependencies.yaml graph drives UI pre-selection, the missing-deps banner, Compose depends_on wiring, and cycle detection.

Schema

The file is a map under a single services: key. Each service may declare a requires: list. Every list entry is a dependency rule:

services:
  ui-backend:
    requires:
      - oneOf: [mysql, oracle]
        mandatory: true
      - service: hazelcast
        mandatory: true
        condition: service_started
      - service: prometheus
        mandatory: false
        autoselect: true
      - service: clickhouse
        mandatory: false
        disablechange: true
        condition: service_healthy
Flag Effect

service: <name>

Fixed dependency on a single other service.

oneOf: [a, b, c]

Exactly one of the listed services must be selected. Resolved from the current DATABASE_TYPE when the options are database variants.

mandatory: true

Without this dependency present in SELECTED_SERVICES, the Create / Update job raises the missing-deps banner. The operator can opt in via ALLOW_MISSING_DEPS.

mandatory: false

Informational only. The dep graph renders the edge; absence does not block the build.

autoselect: true

The Active Choices picker pre-checks the dependency when its parent is selected. Users can still uncheck it unless disablechange: true is also set.

disablechange: true

The checkbox for this service is rendered as read-only. Used for infrastructure that should never be toggled out (hazelcast, clickhouse in many flows).

condition: service_started|service_healthy

Translated into depends_on.<svc>.condition in the rendered Compose file. Must match a Compose V2 valid value — the library does not add its own aliases.

How createEnvStep consumes the graph

The resolution pipeline inside createEnvStep (and mirrored in updateEnvStep):

  1. Load dependencies.yamlutils.toBasic → plain Map.

  2. Resolve every oneOf against DATABASE_TYPE. An entry like oneOf: [mysql, oracle] becomes service: mysql (or whatever was picked).

  3. Walk the requires-tree starting from SELECTED_SERVICES, collecting mandatory deps that are not yet selected.

  4. If any are missing, show the missing-deps banner in FINAL_PREVIEW. Block build unless ALLOW_MISSING_DEPS=true.

  5. Emit depends_on: into the rendered compose file for every resolved edge whose target is in SELECTED_SERVICES.

  6. Run cycle detection (DFS) across the effective graph; print a warning if a cycle is detected. The build continues — Docker Compose itself will refuse to start a circular depends_on.

utils.findMissingMandatoryDeps(selectedServices, databaseType) is the public entry point used by the UI banner.

The missing-deps banner

When findMissingMandatoryDeps returns a non-empty map, FINAL_PREVIEW renders a red banner listing each requesting service and its missing parent:

⚠ ui-backend requires:    hazelcast  (not selected)
⚠ provision-api requires: mysql      (not selected — pick one of mysql/oracle)

The user has two options:

  1. Add the missing services to SELECTED_SERVICES (preferred).

  2. Tick ALLOW_MISSING_DEPS to acknowledge the gap and let the build proceed.

ALLOW_MISSING_DEPS only hides the banner; it does not manufacture phantom services. If the missing dep really is required at runtime, the stack will fail at compose up or health-check time.

UI reflection

  • qaUiScripts.depGraphScript() renders the live SVG graph derived from dependencies.yaml + current selection + DATABASE_TYPE.

  • Clusters in the graph are styled per "family" (databases, monitoring, UI, configuration, emulators). Recent commits rearranged clusters for clarity: 392adc6 split ft-device-network-service, ui-ai-agent, and postgres into dedicated clusters; a0eef12 moved ft-configs-ui into the Configuration cluster.

  • Nodes that are disablechange: true render with a lock glyph; nodes that are mandatory: true render with a solid border.

Cycle detection

Cycle detection is DFS-based and informational — it warns but does not block. The justification: some compose-level cycles are resolved by condition: service_healthy plus real healthchecks, and blocking such envs would be more disruptive than letting Compose reject the deploy with a clearer error.

In practice, a new circular dependency is virtually always a bug. Treat the warning as a build failure unless you have a specific reason to proceed.

Example: adding a rule

Scenario: my-service needs MySQL (or Oracle), plus hazelcast healthy before it can start.

services:
  my-service:
    requires:
      - oneOf: [mysql, oracle]
        mandatory: true
      - service: hazelcast
        mandatory: true
        condition: service_healthy

After saving the file, re-run Create-Environment to verify:

  1. DATABASE_TYPE=mysql, SELECTED_SERVICES includes my-service but not hazelcast → banner fires.

  2. Add hazelcast → banner clears; dep-graph shows an edge.

  3. Rendered compose under MASTER_ENVS_DIR/<env>/docker-compose.yml contains:

    my-service:
      depends_on:
        hazelcast:
          condition: service_healthy
  • utils.findMissingMandatoryDeps(services, dbType) — UI banner source.

  • utils.toBasic(yaml) — collapses the parsed LazyMap to a plain Map before any downstream processing.

  • qaUiScripts.allowMissingDepsScript() — the opt-in checkbox. Only rendered when deps are missing (see commit 2026-04-20-dep-graph-missing-deps-false-banner for the bugfix that scoped the banner properly).