WsCheckpointPublishMojo.java
package network.ike.plugin.ws;
import network.ike.plugin.ReleaseSupport;
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.vcs.VcsOperations;
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.util.ArrayList;
import java.util.List;
/**
* Execute a workspace checkpoint with auto-alignment.
*
* <p>This is the {@code -publish} counterpart of {@code ws:checkpoint}
* (which defaults to a draft preview). The goal's flow is:
* <ol>
* <li>Verify the working trees are clean (user has nothing uncommitted).</li>
* <li>Run {@code ws:align-publish} to apply inter-subproject version
* alignment.</li>
* <li>Commit any tracked changes the alignment step produced under a
* goal-owned message so that the subsequent preflight inside the
* superclass sees a clean tree (IKE-Network/ike-issues#537 — the
* preflight must not fail on changes the goal itself produced).</li>
* <li>Verify the reactor still builds in its aligned-and-committed state
* and refuse to tag on failure (IKE-Network/ike-issues#689 — a
* checkpoint whose pinned subprojects no longer compile together
* must not be cut, otherwise the skew is only discovered across CI
* cycles).</li>
* <li>Delegate to {@link WsCheckpointDraftMojo} with {@code publish = true}
* to do the actual tagging and manifest write.</li>
* </ol>
*
* <p>Usage: {@code mvn ws:checkpoint-publish}
*
* @see WsCheckpointDraftMojo
*/
@Mojo(name = "checkpoint-publish", projectRequired = false, aggregator = true)
public class WsCheckpointPublishMojo extends WsCheckpointDraftMojo {
/** Commit message used for the auto-aligned state, per #537. */
static final String ALIGNMENT_COMMIT_MESSAGE =
"workspace: pre-checkpoint alignment";
/**
* Skip the pre-checkpoint reactor build (#689). Off by default — the
* gate is the whole point. Set {@code -Dws.checkpoint.skipVerify=true}
* to cut a checkpoint without first proving the reactor compiles
* (e.g. when the failure is known and being tracked separately).
*/
@Parameter(property = "ws.checkpoint.skipVerify", defaultValue = "false")
boolean skipVerify;
/**
* Maven goals run against the workspace root to verify the reactor
* builds before tagging (#689). Defaults to {@code clean verify
* -DskipTests -T 1C} — beyond a bare compile it runs the {@code verify}
* phase (packaging, enforcer/convergence checks), so it catches the
* dependency-pin half of the skew that motivated the gate (e.g. a
* lagging {@code komet-bom} elk pin, #687), not just source compile
* errors. Pass a lighter goal set for speed when a compile is enough,
* e.g. {@code -Dws.checkpoint.verifyGoals="clean test-compile -T 1C"}.
*/
@Parameter(property = "ws.checkpoint.verifyGoals",
defaultValue = "clean verify -DskipTests -T 1C")
String verifyGoals;
/** Creates this goal instance. */
public WsCheckpointPublishMojo() {}
@Override
protected WorkspaceReportSpec runGoal() throws MojoException {
publish = true;
// Preflight up front so we know any subsequent dirtiness came
// from our own auto-alignment step, not from work the user
// forgot to commit. This is the first half of the #537 fix —
// it lets us safely auto-commit alignment output without risk
// of sweeping user-staged work into a goal-owned commit.
requireCleanUserState();
autoAlign();
// Second half of #537: alignment can leave the workspace dirty
// (modified POMs for actual alignment changes, or a self-healed
// .gitignore from WorkspaceReport.write). Without this commit,
// the preflight inside super.runGoal() trips on changes we just
// produced and the goal fails itself. Commit them under a
// goal-owned message so the checkpoint records the aligned state.
commitAlignmentSideEffects();
// #689: prove the reactor still builds in its aligned-and-committed
// state before any tag is cut. Runs after the alignment commit so
// the build sees exactly the state the checkpoint will record, and
// before super.runGoal() so a failure prevents every subproject and
// workspace tag — a broken checkpoint never gets created.
verifyReactor();
return super.runGoal();
}
/**
* Run {@code ws:align-publish} as a subprocess against the
* workspace root. Overridable from tests so the regression
* coverage for #537 can simulate alignment side effects without
* invoking a nested Maven build.
*
* @throws MojoException if the subprocess setup fails (subprocess
* failures are caught and logged as warnings —
* the checkpoint proceeds regardless)
*/
protected void autoAlign() throws MojoException {
File root = workspaceRoot();
String mvn = WsReleaseDraftMojo.resolveMvnCommand(root);
getLog().info("Auto-aligning workspace versions...");
try {
// #780: hand the commit to checkpoint — align writes the aligned
// POMs but commitAlignmentSideEffects() below owns committing them
// (and requireCleanUserState() owns the preflight). -Ddefer-commit
// collapses align to DEFER_TO_CALLER so it does not double-commit.
ReleaseSupport.exec(root, getLog(), mvn,
WsGoal.ALIGN_PUBLISH.qualified(), "-Ddefer-commit", "-B");
} catch (MojoException e) {
getLog().warn("Auto-alignment completed with warnings: "
+ e.getMessage());
}
}
/**
* Build the reactor against the workspace root and refuse to cut the
* checkpoint if it fails (IKE-Network/ike-issues#689). Runs {@link
* #verifyGoals} (default {@code clean verify -DskipTests -T 1C}) as a
* subprocess against the aligned-and-committed workspace; a non-zero
* build aborts the goal so no tag is ever created for a checkpoint
* whose pinned subprojects don't compile together.
*
* <p>Unlike {@link #autoAlign()} this is deliberately <em>fail-loud</em>:
* a build failure is the exact condition the gate exists to catch, so
* it is rethrown rather than logged as a warning. Honors {@link
* #skipVerify} as an explicit escape hatch.
*
* <p>Overridable from tests so the #689 gate coverage can drive the
* pass/fail outcome without invoking a nested Maven build.
*
* @throws MojoException if the reactor build fails (and {@link
* #skipVerify} is not set), or if the subprocess
* setup itself fails
*/
protected void verifyReactor() throws MojoException {
if (skipVerify) {
getLog().info("Pre-checkpoint reactor verify skipped "
+ "(ws.checkpoint.skipVerify=true).");
return;
}
File root = workspaceRoot();
String mvn = WsReleaseDraftMojo.resolveMvnCommand(root);
getLog().info("Verifying the reactor builds before tagging ("
+ verifyGoals + ") ...");
List<String> cmd = new ArrayList<>();
cmd.add(mvn);
for (String token : verifyGoals.split("\\s+")) {
if (!token.isBlank()) {
cmd.add(token);
}
}
cmd.add("-B");
try {
ReleaseSupport.exec(root, getLog(), cmd.toArray(new String[0]));
} catch (MojoException e) {
throw new MojoException(
"Pre-checkpoint reactor build failed — refusing to cut "
+ "the checkpoint. Fix the build, or re-run with "
+ "-Dws.checkpoint.skipVerify=true. Cause: "
+ e.getMessage(), e);
}
}
/**
* Run the working-tree-clean preflight against user state before
* we touch the workspace. Same condition the superclass enforces
* inside its own {@code runGoal}; running it here ensures that any
* dirtiness observed after {@link #autoAlign()} originated from the
* goal's own work rather than from work the user forgot to commit.
*
* @throws MojoException if any working tree has uncommitted changes
*/
private void requireCleanUserState() throws MojoException {
WorkspaceGraph graph = loadGraph();
File root = workspaceRoot();
Preflight.of(
List.of(PreflightCondition.WORKING_TREE_CLEAN),
PreflightContext.of(root, graph, graph.topologicalSort()))
.requirePassed(WsGoal.CHECKPOINT_PUBLISH);
}
/**
* Commit any tracked-but-uncommitted changes left behind by
* {@link #autoAlign()} under {@link #ALIGNMENT_COMMIT_MESSAGE}.
* Iterates the workspace root and each subproject, no-op when the
* tree is clean. Per-subproject failures are reported as warnings
* so that one bad subproject doesn't abort the checkpoint (the
* subsequent preflight will still fail loudly if anything remains
* dirty).
*/
private void commitAlignmentSideEffects() {
File root = workspaceRoot();
WorkspaceGraph graph;
try {
graph = loadGraph();
} catch (MojoException e) {
getLog().warn("Could not load graph for pre-checkpoint commit: "
+ e.getMessage());
return;
}
if (new File(root, ".git").exists()) {
commitIfDirty(root, "workspace root");
}
for (String name : graph.topologicalSort()) {
File dir = new File(root, name);
if (new File(dir, ".git").exists()) {
commitIfDirty(dir, name);
}
}
}
private void commitIfDirty(File dir, String label) {
if (VcsOperations.isClean(dir)) {
return;
}
try {
VcsOperations.addAll(dir, getLog());
VcsOperations.commit(dir, getLog(), ALIGNMENT_COMMIT_MESSAGE);
getLog().info(" Auto-committed pre-checkpoint alignment in "
+ label);
} catch (MojoException e) {
getLog().warn("Could not auto-commit pre-checkpoint alignment in "
+ label + ": " + e.getMessage());
}
}
}