WsAlignDraftMojo.java
package network.ike.plugin.ws;
import network.ike.plugin.ws.preflight.Preflight;
import network.ike.plugin.ws.preflight.PreflightCondition;
import network.ike.plugin.ws.preflight.PreflightContext;
import network.ike.plugin.ws.preflight.PreflightResult;
import network.ike.plugin.ws.reconcile.AlignmentReconciler;
import network.ike.plugin.ws.reconcile.DriftReport;
import network.ike.plugin.ws.reconcile.ReconcilerOptions;
import network.ike.plugin.ws.reconcile.WorkspaceContext;
import network.ike.workspace.ManifestReader;
import network.ike.workspace.WorkspaceGraph;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Mojo;
import org.apache.maven.api.plugin.annotations.Parameter;
import java.io.File;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Preview inter-subproject dependency, plugin, and parent version
* alignment across every cloned subproject.
*
* <p>This is the {@code ws:align-draft} goal — a thin wrapper over
* {@link AlignmentReconciler}. The same reconciler is iterated from
* {@code ws:scaffold-{draft,publish}} (#393); this standalone goal
* exists for the "I detected misalignment, run only that" use case
* where iterating the full convergence pass would be needlessly
* broad.
*
* <p>This goal also serves as the designated entry point for the
* legacy schema migration ({@code components:} → {@code subprojects:},
* #150): it calls
* {@link ManifestReader#migrateLegacySchemaIfNeeded} before reading
* the graph so old manifests are rewritten in place.
*
* <p>Branch reconciliation (the rare "manifest ↔ git state" recovery
* operation that used to share this goal as
* {@code -Dscope=branches}) moved to
* {@link WsReconcileBranchesDraftMojo} ({@code ws:reconcile-branches-draft})
* per the ike-issues#200 split. The {@code scope} parameter on this
* goal is retained only to emit a clear migration error for users
* with muscle memory.
*
* <pre>{@code
* mvn ws:align-draft # preview POM version updates
* mvn ws:align-publish # apply
* }</pre>
*
* @see AlignmentReconciler
* @see WsReconcileBranchesDraftMojo
*/
@Mojo(name = "align-draft", projectRequired = false, aggregator = true)
public class WsAlignDraftMojo extends AbstractWorkspaceMojo {
/**
* When true, apply changes; when false (default), preview only.
* Package-private so {@link WsAlignPublishMojo} can flip it.
*/
@Parameter(property = "publish", defaultValue = "false")
boolean publish;
/**
* Migration aid for the ike-issues#200 two-axis split. The only
* accepted value is {@code poms} (the new default). Passing
* {@code branches} or {@code all} throws with a pointer to
* {@code ws:reconcile-branches-{draft,publish}}, so users with
* muscle memory get a clear error rather than silent breakage.
*/
@Parameter(property = "scope", defaultValue = "poms")
String scope;
/** Creates this goal instance. */
public WsAlignDraftMojo() {}
@Override
protected WorkspaceReportSpec runGoal() throws MojoException {
boolean draft = !publish;
// Migration gate (ike-issues#200): the align goals are POM-only.
// Branch reconciliation moved to ws:reconcile-branches-{draft,publish}.
if ("branches".equals(scope) || "all".equals(scope)) {
String invokedGoal = "ws:align-" + (publish ? "publish" : "draft");
throw new MojoException(
invokedGoal + " no longer supports -Dscope=" + scope
+ " (ike-issues#200). Branch reconciliation moved to:\n"
+ " mvn ws:reconcile-branches-"
+ (publish ? "publish" : "draft")
+ "\n " + invokedGoal
+ " is now POM-only — drop -Dscope=.");
}
if (!"poms".equals(scope)) {
throw new MojoException(
"Invalid scope '" + scope + "' — expected poms");
}
// #150 migration: ws:align-publish (and the draft variant) is the
// designated entry point that rewrites legacy schemas in place.
// Every other reader hits the hard-cut in ManifestReader.read, so
// users on an old workspace see a message pointing them at
// ws:align-publish first. Do this BEFORE loadGraph() so graph
// construction sees the migrated content.
Path manifestPath = resolveManifest();
ManifestReader.migrateLegacySchemaIfNeeded(
manifestPath, msg -> getLog().info(" " + msg));
WorkspaceGraph graph = loadGraph();
File root = workspaceRoot();
getLog().info("");
getLog().info("IKE Workspace Align — reconcile inter-subproject versions");
getLog().info("══════════════════════════════════════════════════════════════");
if (draft) {
getLog().info(" (draft — no files will be modified)");
}
getLog().info("");
// POM alignment requires clean working trees (#132) so the
// rewrite commit doesn't bundle unrelated edits.
List<String> sorted = graph.topologicalSort();
PreflightResult preflight = Preflight.of(
List.of(PreflightCondition.WORKING_TREE_CLEAN),
PreflightContext.of(root, graph, sorted));
if (draft) {
preflight.warnIfFailed(getLog(), WsGoal.ALIGN_PUBLISH);
} else {
preflight.requirePassed(WsGoal.ALIGN_PUBLISH);
}
WorkspaceContext ctx = new WorkspaceContext(
root, manifestPath, graph,
readReconcilerOptions(), getLog());
AlignmentReconciler reconciler = new AlignmentReconciler();
String content;
if (draft) {
DriftReport report = reconciler.detect(ctx);
printDriftReport(report);
content = report.toMarkdown();
} else {
reconciler.apply(ctx);
content = "Inter-subproject version alignment applied across "
+ "the workspace.\n";
}
getLog().info("");
return new WorkspaceReportSpec(
publish ? WsGoal.ALIGN_PUBLISH : WsGoal.ALIGN_DRAFT, content);
}
/**
* Collect Maven system properties into a {@link ReconcilerOptions}
* bag so the reconciler can query its opt-out flag
* ({@code -DupdateAlignment=false}).
*/
private static ReconcilerOptions readReconcilerOptions() {
Map<String, String> flags = new HashMap<>();
for (String name : System.getProperties().stringPropertyNames()) {
flags.put(name, System.getProperty(name));
}
return new ReconcilerOptions(flags);
}
/**
* Render a {@link DriftReport} for {@code ws:align-draft} output
* with the copy-paste opt-out command inline. Mirrors the format
* used by {@code ws:scaffold-draft}'s reconciler loop.
*/
private void printDriftReport(DriftReport report) {
if (!report.hasDrift()) {
getLog().info(" ✓ " + report.dimension());
return;
}
getLog().info(" ⚠ " + report.dimension());
if (!report.summary().isEmpty()) {
getLog().info(" " + report.summary());
}
for (String line : report.detailLines()) {
getLog().info(" " + line);
}
if (!report.defaultAction().isEmpty()) {
getLog().info(" Default: " + report.defaultAction());
}
if (!report.optOutCommand().isEmpty()) {
getLog().info(" Opt out: " + report.optOutCommand());
}
}
}