WsPostReleaseMojo.java

package network.ike.plugin.ws;

import network.ike.plugin.ReleaseSupport;

import network.ike.workspace.Subproject;
import network.ike.workspace.ManifestWriter;
import network.ike.workspace.WorkspaceGraph;
import network.ike.plugin.ws.vcs.VcsOperations;
import network.ike.plugin.ws.vcs.VcsState;
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.io.IOException;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;

/**
 * Post-release version bump across workspace subprojects.
 *
 * <p>After a release, this goal bumps every checked-out subproject's
 * POM version to the specified {@code nextVersion}, commits the
 * change, pushes if a remote exists, then updates workspace.yaml
 * to reflect the new development versions.
 *
 * <p>Components are processed in topological order so that upstream
 * dependencies are bumped before downstream consumers.
 *
 * <pre>{@code
 * mvn ike:post-release -DnextVersion=4-SNAPSHOT
 * }</pre>
 */
@Mojo(name = "post-release", projectRequired = false, aggregator = true)
public class WsPostReleaseMojo extends AbstractWorkspaceMojo {

    /**
     * The next development version to set across all subprojects,
     * e.g., {@code "4-SNAPSHOT"}.
     */
    @Parameter(property = "nextVersion")
    String nextVersion;

    /** Creates this goal instance. */
    public WsPostReleaseMojo() {}

    @Override
    protected WorkspaceReportSpec runGoal() throws MojoException {
        nextVersion = requireParam(nextVersion, "nextVersion",
                "Next development version (e.g., 4-SNAPSHOT)");
        validateMavenVersion(nextVersion);

        WorkspaceGraph graph = loadGraph();
        File root = workspaceRoot();
        Path manifestPath = resolveManifest();

        // VCS bridge: catch-up before modifying
        VcsOperations.catchUp(root, getLog());

        List<String> sorted = graph.topologicalSort(
                new LinkedHashSet<>(graph.manifest().subprojects().keySet()));

        getLog().info("");
        getLog().info("IKE Workspace \u2014 Post-Release");
        getLog().info("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
        getLog().info("  Next version: " + nextVersion);
        getLog().info("  Components:   " + sorted.size());
        getLog().info("");

        Map<String, String> versionUpdates = new LinkedHashMap<>();
        int bumped = 0;
        int skipped = 0;

        for (String name : sorted) {
            Subproject subproject = graph.manifest().subprojects().get(name);
            File dir = new File(root, name);
            File gitDir = new File(dir, ".git");
            File pomFile = new File(dir, "pom.xml");

            if (!gitDir.exists()) {
                getLog().info("  \u26A0 " + name + " \u2014 not cloned, skipping");
                skipped++;
                continue;
            }

            if (!pomFile.exists()) {
                getLog().info("  \u26A0 " + name + " \u2014 no pom.xml, skipping");
                skipped++;
                continue;
            }

            // Read current version from root POM
            String currentVersion;
            try {
                currentVersion = ReleaseSupport.readPomVersion(pomFile);
            } catch (MojoException e) {
                getLog().warn("  \u26A0 " + name + " \u2014 could not read version: "
                        + e.getMessage());
                skipped++;
                continue;
            }

            // Idempotency guard (#294): re-running with the same
            // -DnextVersion on a subproject already at that version is a
            // no-op \u2014 log and skip rather than running the rewrite +
            // commit machinery only to discover there's nothing to do.
            if (nextVersion.equals(currentVersion)) {
                getLog().info("  \u2713 " + name + " \u2014 already at "
                        + nextVersion);
                skipped++;
                continue;
            }

            getLog().info("  \u2192 " + name + " \u2014 " + currentVersion
                    + " \u2192 " + nextVersion);

            // Set version to nextVersion in POM
            ReleaseSupport.setPomVersion(pomFile, currentVersion, nextVersion);

            // Also update submodule POMs that reference the old version
            try {
                List<File> allPoms = ReleaseSupport.findPomFiles(dir);
                for (File subPom : allPoms) {
                    if (subPom.equals(pomFile)) continue;
                    try {
                        String content = java.nio.file.Files.readString(
                                subPom.toPath(), java.nio.charset.StandardCharsets.UTF_8);
                        if (content.contains("<version>" + currentVersion + "</version>")) {
                            String updated = content.replace(
                                    "<version>" + currentVersion + "</version>",
                                    "<version>" + nextVersion + "</version>");
                            java.nio.file.Files.writeString(
                                    subPom.toPath(), updated,
                                    java.nio.charset.StandardCharsets.UTF_8);
                            String rel = dir.toPath().relativize(subPom.toPath()).toString();
                            getLog().info("    updated: " + rel);
                        }
                    } catch (java.io.IOException e) {
                        getLog().warn("    Could not update " + subPom + ": "
                                + e.getMessage());
                    }
                }
            } catch (MojoException e) {
                getLog().warn("    Could not scan submodule POMs: " + e.getMessage());
            }

            // Commit: git add pom.xml && git commit
            ReleaseSupport.exec(dir, getLog(), "git", "add", "pom.xml");
            // Stage any submodule POMs that were updated
            try {
                List<File> allPoms = ReleaseSupport.findPomFiles(dir);
                for (File subPom : allPoms) {
                    if (!subPom.equals(pomFile)) {
                        String rel = dir.toPath().relativize(subPom.toPath()).toString();
                        ReleaseSupport.exec(dir, getLog(), "git", "add", rel);
                    }
                }
            } catch (MojoException e) {
                getLog().debug("Could not stage submodule POMs: " + e.getMessage());
            }
            VcsOperations.commitStaged(dir, getLog(),
                    "post-release: bump to " + nextVersion);

            // Push if remote exists (safe — ignores failure)
            VcsOperations.pushIfRemoteExists(dir, getLog(), "origin",
                    gitBranch(dir));

            versionUpdates.put(name, nextVersion);
            bumped++;
        }

        // Update workspace.yaml versions
        if (!versionUpdates.isEmpty()) {
            try {
                ManifestWriter.updateMavenVersions(manifestPath, versionUpdates);
                getLog().info("");
                getLog().info("  Updated workspace.yaml versions for "
                        + versionUpdates.size() + " components");
            } catch (IOException e) {
                throw new MojoException(
                        "Failed to update workspace.yaml: " + e.getMessage(), e);
            }

            // Commit workspace.yaml on aggregator
            File wsRoot = manifestPath.getParent().toFile();
            File wsGit = new File(wsRoot, ".git");
            if (wsGit.exists()) {
                ReleaseSupport.exec(wsRoot, getLog(), "git", "add", "workspace.yaml");
                VcsOperations.commitStaged(wsRoot, getLog(),
                        "post-release: bump workspace versions to " + nextVersion);
                VcsOperations.pushIfRemoteExists(wsRoot, getLog(), "origin",
                        gitBranch(wsRoot));
            }
        }

        getLog().info("");
        getLog().info("  Bumped: " + bumped + " | Skipped: " + skipped);
        getLog().info("");

        return new WorkspaceReportSpec(WsGoal.POST_RELEASE,
                "**" + bumped + "** bumped, **"
                + skipped + "** skipped.\n");
    }
}