ReleaseCascade.java
package network.ike.workspace.cascade;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* The assembled IKE release cascade graph — the full cross-repo
* release ordering, stitched from the per-project
* {@code release-cascade.yaml} manifests by {@link CascadeAssembler}
* (IKE-Network/ike-issues#402, #420).
*
* <p>No project authors this ordering. Each project version-controls
* only its own {@code upstream}/{@code downstream} edges; the
* assembler traverses those edges and topologically orders the
* result. The {@link #repos()} list is that order: a node always
* follows every node it consumes.
*
* <p>Helper methods answer the questions the release goals ask:
* <ul>
* <li>{@link #downstreamOf(String)} — "I just released this
* groupId; what is now stale?"</li>
* <li>{@link #find(String)} / {@link #findByCoordinates(String,
* String)} — resolve a cascade node from a project's own POM
* coordinates.</li>
* </ul>
*
* @param repos cascade nodes in topological order; never {@code null}
*/
public record ReleaseCascade(List<CascadeRepo> repos) {
/**
* Canonical constructor — defensively copies {@code repos} and
* substitutes an empty list for {@code null}.
*/
public ReleaseCascade {
repos = repos == null ? List.of() : List.copyOf(repos);
}
/**
* Looks up a cascade node by Maven {@code groupId}.
*
* @param groupId the groupId to find
* @return the matching node, or empty if {@code groupId} is not a
* cascade member
*/
public Optional<CascadeRepo> find(String groupId) {
return repos.stream()
.filter(r -> r.groupId().equals(groupId))
.findFirst();
}
/**
* Looks up a cascade node by exact {@code groupId} +
* {@code artifactId} coordinates.
*
* @param groupId the project's groupId
* @param artifactId the project's artifactId
* @return the matching node, or empty if the coordinates are not a
* cascade member
*/
public Optional<CascadeRepo> findByCoordinates(String groupId,
String artifactId) {
return repos.stream()
.filter(r -> r.groupId().equals(groupId)
&& r.artifactId().equals(artifactId))
.findFirst();
}
/**
* Tests whether a {@code groupId} participates in the cascade.
*
* @param groupId the groupId
* @return true if {@code groupId} is a cascade member
*/
public boolean contains(String groupId) {
return find(groupId).isPresent();
}
/**
* Returns the cascade members reachable downstream of
* {@code groupId}, in cascade (topological) order.
*
* <p>These are exactly the repos that go stale when
* {@code groupId} is released: each one consumes, directly or
* through an intermediate, its artifacts. The traversal follows
* the {@code downstream} edges of each node.
*
* @param groupId the groupId that was (or will be) released
* @return downstream nodes in release order; empty if
* {@code groupId} has no consumers or is not a member
*/
public List<CascadeRepo> downstreamOf(String groupId) {
Set<String> stale = new LinkedHashSet<>();
Deque<String> frontier = new ArrayDeque<>();
frontier.add(groupId);
while (!frontier.isEmpty()) {
String current = frontier.poll();
find(current).ifPresent(node -> {
for (CascadeEdge edge : node.downstream()) {
if (stale.add(edge.groupId())) {
frontier.add(edge.groupId());
}
}
});
}
List<CascadeRepo> ordered = new ArrayList<>();
for (CascadeRepo r : repos) {
if (stale.contains(r.groupId())) {
ordered.add(r);
}
}
return ordered;
}
}