Skip to content

Module Dependency System

Overview

The module dependency system allows modules to declare relationships to other modules via depends_on in repository.yml. Dependencies are used for graph visualization, validation, cache invalidation, and release gating.

YAML Schema

# In .r2r/eac/repository.yml
modules:
  - moniker: eac-commands
    depends_on:
      - eac-core

Source: go/eac/core/contracts/types.go

type BaseContract struct {
    DependsOn []string `yaml:"depends_on"`
}

Data Flow

repository.yml
modules.LoadFromWorkspace()
Registry (holds all ModuleContract)
     ├──► GetDependencyGraph()        → map[string][]string (module → dependencies)
     ├──► GetReverseDependencyGraph() → map[string][]string (module → dependents)
     └──► CalculateExecutionOrder()   → ExecutionPlan (layers + flat order)

Key Files

File Purpose
go/eac/core/contracts/types.go BaseContract.DependsOn - YAML parsing
go/eac/core/contracts/modules/types.go ModuleContract.GetDependencies() - accessor
go/eac/core/repository/dependencies.go Graph operations, execution order
go/eac/commands/impl/validate/module-hierarchy.go Cycle detection
go/eac/commands/impl/release/await-deps.go Release-time CI verification

Execution Order Algorithm

File: go/eac/core/repository/dependencies.go

Uses Kahn's topological sort:

  1. Build in-degree map (count of unprocessed dependencies per module)
  2. Layer 0 = modules with in-degree 0 (no dependencies)
  3. Process layer, decrease in-degrees of dependents
  4. Repeat until all modules processed
  5. Detect cycles if layer is empty but modules remain
type ExecutionPlan struct {
    Layers         [][]string // [[eac-core], [eac-commands, r2r-cli], [ext-eac]]
    ExecutionOrder []string   // Flattened: [eac-core, eac-commands, r2r-cli, ext-eac]
    LayerCount     int
}

Cycle Detection

File: go/eac/commands/impl/validate/module-hierarchy.go

Uses DFS with recursion stack:

func validateNoCircularDependencies(reg *modules.Registry, report *moduleHierarchyReport) {
    visited := make(map[string]bool)
    recStack := make(map[string]bool)  // Tracks current path

    // DFS - if we hit a node already in recStack, we found a cycle
    var detectCycle func(moniker string, path []string) bool
}

Dependency Uses

Use Case Description
Graph validation Checked for cycles, missing references
Build order Optional layer-sequential execution with --layered-build
Cache invalidation Dependents rebuild when dependency changes
Release gating Block release if dependency CI failed

Build Behavior

Default:           All modules run in parallel (up to MaxConcurrency)
--layered-build:   Layer-sequential execution (Layer 0, then Layer 1, etc.)
--no-deps:         Isolated build - only build specified module, no dependency resolution

CI Isolation

Each CI workflow builds its module in isolation - no cross-CI artifact downloads, no dependency resolution.

Build Command Flag

build <module> --no-deps    # Build only this module, skip dependency handling

The --no-deps flag: - Skips downloading artifacts from other CI workflows - Skips adding transitive dependencies to execution plan - Builds only the explicitly specified module(s)

build-module Action

The build-module action defaults to isolated mode (no-deps: true).

# Default: isolated build (no cross-CI dependencies)
- uses: ./.github/actions/build-module
  with:
    module: eac-commands

# Explicit: build with dependencies (for integration tests)
- uses: ./.github/actions/build-module
  with:
    module: r2r-installer
    no-deps: 'false'  # Download dependency artifacts from other CI runs

Why CI Isolation?

  1. Speed: Each CI runs independently, no waiting for other workflows
  2. Reliability: CI failures are isolated to their module
  3. Simplicity: No complex artifact download chains between workflows
  4. Correct gating: Release-time verification (await-deps) ensures dependencies passed

CI and Release Gating

CI workflows run in parallel for all changed modules. Release workflows verify dependencies before allowing release.

CI Dispatch

change-trigger.yaml
    └──► Dispatch CI workflows in parallel
         [eac-core, eac-commands, r2r-cli, ext-eac, docs, books, ...]
         No layering, no waiting

Release Verification

release-ext-eac.yaml
    └──► approve-release action
            ├──► release check-ci (verify own CI passed)
            └──► await-dependency-ci action
                    └──► release await-deps ext-eac
                            ├──► Get transitive deps: [eac-commands, eac-core]
                            ├──► For each dependency:
                            │       ├──► Find last commit that changed it
                            │       ├──► Verify CI passed for that commit
                            │       └──► If in-progress: wait; If failed: block
                            └──► All passed → allow release

await-deps Command

File: go/eac/commands/impl/release/await-deps.go

release await-deps <module> [--timeout N] [--skip-static]

1. Load module registry
2. Get transitive dependencies of <module>
3. For each dependency:
   a. Find last commit that changed dep's files (git log -1)
   b. Check CI status for ci-<dep>.yaml at that commit
      - Success: continue
      - In-progress: wait (with timeout)
      - Failed: exit 1 with error
      - Not found: check if dep has CI workflow
4. All passed → exit 0

Output example:

Awaiting CI for ext-eac dependencies...

Dependencies: eac-commands, eac-core

  Checking eac-commands
    Last changed: abc1234 (feat: add new feature)
    CI run: #12345 ✓ passed

  Checking eac-core
    Last changed: def5678 (fix: resolve issue)
    CI run: #12340 ✓ passed

✓ All 2 dependency CI checks passed

Example Dependency Graph

eac-core (root)
    ├──► eac-commands ──┬──► ext-eac
    │        │          │
    │        │          │
    │        ├──► docs ──► books
    │        │
    │        └──► implicit-r2r-cli
    ├──► r2r-cli ──┬──► ext-eac (also depends on eac-commands)
    │              │
    │              └──► r2r-installer
    ├──► eac-specs
    └──► eac-mcp-commands