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
* artifact; 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>
*
* <p>Identity is the {@code groupId:artifactId} pair, NOT
* {@code groupId} alone. Foundation members can share a groupId
* (e.g., {@code network.ike.tooling:ike-tooling} and
* {@code network.ike.tooling:ike-workspace-extension} both live
* under the same group but are independent cascade heads with
* different downstream edges). Lookups take the GA string
* ({@code "groupId:artifactId"}) — see
* {@link CascadeRepo#ga()} — or both coordinates explicitly via
* {@link #findByCoordinates(String, String)}
* (IKE-Network/ike-issues#466).
*
* @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 its {@code "groupId:artifactId"} GA
* string.
*
* @param ga the {@link CascadeRepo#ga()} of the node to find
* @return the matching node, or empty if {@code ga} is not a
* cascade member
*/
public Optional<CascadeRepo> find(String ga) {
return repos.stream()
.filter(r -> r.ga().equals(ga))
.findFirst();
}
/**
* Looks up a cascade node by exact {@link MavenCoordinate}.
*
* @param coordinate the project's coordinate
* @return the matching node, or empty if the coordinate is not a
* cascade member
*/
public Optional<CascadeRepo> findByCoordinates(
MavenCoordinate coordinate) {
return repos.stream()
.filter(r -> r.coordinate().equals(coordinate))
.findFirst();
}
/**
* Convenience overload — wraps the two-String pair into a
* {@link MavenCoordinate} and delegates.
*
* @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 findByCoordinates(new MavenCoordinate(groupId, artifactId));
}
/**
* Tests whether a {@code ga} participates in the cascade.
*
* @param ga the {@link CascadeRepo#ga()} to test
* @return true if {@code ga} is a cascade member
*/
public boolean contains(String ga) {
return find(ga).isPresent();
}
/**
* Returns the cascade members reachable downstream of {@code ga},
* in cascade (topological) order.
*
* <p>These are exactly the repos that go stale when {@code ga} is
* released: each one consumes, directly or through an
* intermediate, its artifacts. The traversal follows the
* {@code downstream} edges of each node.
*
* @param ga the {@link CascadeRepo#ga()} that was (or will be)
* released
* @return downstream nodes in release order; empty if {@code ga}
* has no consumers or is not a member
*/
public List<CascadeRepo> downstreamOf(String ga) {
Set<String> stale = new LinkedHashSet<>();
Deque<String> frontier = new ArrayDeque<>();
frontier.add(ga);
while (!frontier.isEmpty()) {
String current = frontier.poll();
find(current).ifPresent(node -> {
for (CascadeEdge edge : node.downstream()) {
if (stale.add(edge.ga())) {
frontier.add(edge.ga());
}
}
});
}
List<CascadeRepo> ordered = new ArrayList<>();
for (CascadeRepo r : repos) {
if (stale.contains(r.ga())) {
ordered.add(r);
}
}
return ordered;
}
}