How Jenkins Implements Dependency Graph Tracking with DependencyGraph for Build Triggers

Jenkins maintains a bi-directional, immutable dependency graph via hudson.model.DependencyGraph that rebuilds when configurations change, with edges contributed by components implementing the DependencyDeclarer interface.

The jenkinsci/jenkins repository implements sophisticated dependency graph tracking to orchestrate complex build pipelines and trigger downstream jobs automatically. At the center of this mechanism is the DependencyGraph class, which stores relationships between AbstractProject instances and determines when downstream builds should fire based on upstream completion events.

Core Architecture of the DependencyGraph

Graph Construction and Immutability

When Jenkins starts or when Jenkins.rebuildDependencyGraph() is called, the system creates a fresh DependencyGraph instance. Inside DependencyGraph.build() (lines 85‑92 of core/src/main/java/hudson/model/DependencyGraph.java), the method enumerates all items of type AbstractProject and asks each project to contribute its edges:

for (AbstractProject p : Jenkins.get().allItems(AbstractProject.class))
    p.buildDependencyGraph(this);

The AbstractProject.buildDependencyGraph(DependencyGraph) method delegates to the project's publishers, builders, wrappers, and triggers. Each component can implement jenkins.model.DependencyDeclarer to hook into this process and register dependencies.

Bidirectional Edge Storage

The graph stores relationships in two concurrent hash maps: forward (upstream to downstream) and backward (downstream to upstream). When a component calls graph.addDependency(Dependency), the method populates both maps simultaneously to enable efficient traversal in either direction:

public void addDependency(Dependency dep) {
    if (built) throw new IllegalStateException();
    add(forward, dep.getUpstreamProject(), dep);
    add(backward, dep.getDownstreamProject(), dep);
}

The Dependency object records the upstream and downstream projects and defines shouldTriggerBuild, which determines whether a downstream build should start when the upstream finishes.

How Build Triggers Register Dependencies

The DependencyDeclarer Interface

Plugins and core components declare dependencies by implementing jenkins.model.DependencyDeclarer. This interface provides a single method, buildDependencyGraph(AbstractProject owner, DependencyGraph graph), which Jenkins calls during graph reconstruction. Implementations add edges by calling graph.addDependency() with a Dependency subclass that encapsulates custom trigger logic.

ReverseBuildTrigger Implementation

ReverseBuildTrigger in core/src/main/java/jenkins/triggers/ReverseBuildTrigger.java (lines 72‑79) demonstrates how a trigger living on the downstream job registers a dependency on upstream completion:

@Override public void buildDependencyGraph(final AbstractProject downstream, DependencyGraph graph) {
    for (AbstractProject upstream :
         Items.fromNameList(downstream.getParent(), getUpstreamProjects(), AbstractProject.class)) {
        graph.addDependency(new DependencyGraph.Dependency(upstream, downstream) {
            @Override public boolean shouldTriggerBuild(AbstractBuild upstreamBuild,
                                                          TaskListener listener, List<Action> actions) {
                return shouldTrigger(upstreamBuild, listener);
            }
        });
    }
}

This trigger creates an anonymous Dependency subclass that overrides shouldTriggerBuild to check the upstream build result against configured thresholds.

Trigger Execution Flow

When an upstream build completes, the queue runner invokes Dependency.Dependency.shouldTriggerBuild. For ReverseBuildTrigger, the actual scheduling happens in ReverseBuildTrigger.RunListenerImpl.onCompleted (lines 72‑99):

if (trigger.shouldTrigger(r, listener)) {
    ParameterizedJobMixIn.scheduleBuild2(trigger.job, -1,
        new CauseAction(new Cause.UpstreamCause(r)));
}

The listener retrieves cached triggers for the completed build’s project, evaluates shouldTrigger, and schedules the downstream job via ParameterizedJobMixIn.scheduleBuild2 with an UpstreamCause.

Topological Ordering and Cycle Detection

After all projects contribute edges, DependencyGraph.build() finalizes the forward and backward maps, wraps them in unmodifiable collections, and executes Tarjan’s strongly connected components algorithm (lines 101‑135). This computes a stable topologicalOrder used for project comparison via DependencyGraph.compare() and for detecting circular dependencies in the job hierarchy.

Graph Rebuild Lifecycle

When and How the Graph Rebuilds

Reconstruction occurs in two scenarios:

  • Manual request – Calling Jenkins.get().rebuildDependencyGraph() (or the asynchronous rebuildDependencyGraphAsync())
  • Configuration change – Most configurables invoke rebuildDependencyGraphAsync() after persisting changes to disk

Because the graph is immutable after construction, Jenkins atomically replaces the old instance with a new one without requiring synchronization locks, making the rebuild process thread-safe and cheap.

Querying Dependencies at Runtime

Developers can access the current graph to inspect relationships programmatically:

// Force a rebuild (e.g., from a system script)
Jenkins.get().rebuildDependencyGraph();   // triggers a fresh DependencyGraph build

// Implement a simple DependencyDeclarer (e.g., a custom publisher)
public class MyPublisher extends Recorder implements DependencyDeclarer {
    @Override
    public void buildDependencyGraph(AbstractProject owner, DependencyGraph graph) {
        // add an edge from a static upstream job to the current job
        Job<?,?> upstream = Jenkins.get().getItem("my-upstream", Job.class);
        if (upstream != null) {
            graph.addDependency(new DependencyGraph.Dependency(upstream, owner));
        }
    }
}

// Query the graph at runtime
DependencyGraph dg = Jenkins.get().getDependencyGraph();
List<AbstractProject> downstream = dg.getDownstream(someProject);
System.out.println("Downstream jobs: " + downstream);

Summary

  • Jenkins uses an immutable DependencyGraph stored in hudson.model.DependencyGraph to track bi-directional job relationships
  • The DependencyDeclarer interface in jenkins.model allows publishers, builders, and triggers to contribute edges during graph construction
  • ReverseBuildTrigger exemplifies downstream trigger logic by implementing custom shouldTriggerBuild logic in jenkins/triggers/ReverseBuildTrigger.java
  • Graph reconstruction happens atomically via Jenkins.rebuildDependencyGraph() or asynchronously after configuration changes
  • Tarjan’s SCC algorithm provides topological sorting and cycle detection across the project hierarchy

Frequently Asked Questions

How does Jenkins detect circular dependencies between jobs?

The DependencyGraph class performs a topological sort using Tarjan’s strongly connected components algorithm during the build() method. Strongly connected components indicate cycles, which Jenkins handles by computing a stable topologicalOrder that enforces a consistent project ranking and prevents infinite trigger loops.

Can plugins declare custom dependencies without modifying core Jenkins code?

Yes. Plugins implement the jenkins.model.DependencyDeclarer interface on their extensions (such as publishers or triggers). During graph reconstruction, Jenkins automatically invokes buildDependencyGraph() on these components, allowing them to call graph.addDependency() with custom Dependency objects that define specific trigger conditions via shouldTriggerBuild().

What happens to running builds when the dependency graph is rebuilt?

Running builds remain unaffected because DependencyGraph is immutable after construction. When rebuildDependencyGraph() executes, it creates a completely new instance and atomically swaps the reference in the Jenkins singleton. Existing builds continue referencing the graph instance that existed at their start, ensuring thread safety without requiring synchronization locks.

How does ReverseBuildTrigger determine whether to fire a downstream build?

The trigger implements a custom shouldTriggerBuild() method that evaluates the upstream build result (e.g., checking if it matches Result.SUCCESS or other configured thresholds) and verifies user permissions. The RunListenerImpl invokes this check upon upstream completion in onCompleted(), and only calls ParameterizedJobMixIn.scheduleBuild2() if the method returns true.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →