FinalizePhase.java
package network.ike.plugin.release.finalize;
import network.ike.plugin.ReleaseNotesSupport;
import network.ike.plugin.ReleaseSupport;
import network.ike.plugin.release.ReleaseContext;
import org.apache.maven.api.plugin.MojoException;
import java.io.File;
import java.nio.file.Path;
/**
* The finalize phase of the release pipeline — the last steps that
* make the release externally visible beyond the Nexus deploy.
*
* <p>Runs after the {@code WorktreeGuard} has restored the worktree
* to main and after the Nexus deploy (and best-effort Central deploy)
* have completed. Performs:
*
* <ul>
* <li>B29 — pushes the release tag and main to origin
* <li>B30 — creates the GitHub Release with milestone-based notes,
* closes the milestone, removes pending-release labels from
* resolved issues
* </ul>
*
* <p>The post-deploy log lines ("Release v X complete", Nexus/Central
* outcome summary, GitHub Pages URLs) remain on the mojo for now —
* they consume outcomes from multiple phases (Nexus, Central, Site)
* and naturally land in {@code ReleaseExecution} when the orchestrator
* is wired up. The reporting helpers ({@code reportCascade},
* {@code buildReleaseReport}) also stay on the mojo because the draft
* path invokes them too; they extract together in Commit 6.
*
* <p>Carved out of {@code ReleaseDraftMojo} during the Phase 4
* Commit 1 (IKE-Network/ike-issues#489).
*/
public final class FinalizePhase {
private final ReleaseContext ctx;
/**
* Creates a new finalize phase bound to the given context.
*
* @param ctx the per-invocation release context
*/
public FinalizePhase(ReleaseContext ctx) {
this.ctx = ctx;
}
/**
* Executes the finalize phase.
*
* <p>If {@link FinalizeInput#hasOrigin()} is {@code false}, pushes
* and the GitHub Release step are skipped (with informational log
* messages) — appropriate for a local-only release where no
* remote is configured.
*
* @param input the inputs accumulated by {@code runGoal()} through the prior phases
* @return a {@link FinalizeOutcome} recording which steps ran
* @throws MojoException if either {@code git push} fails
*/
public FinalizeOutcome execute(FinalizeInput input) throws MojoException {
boolean tagPushed = false;
boolean mainPushed = false;
boolean githubReleaseAttempted = false;
if (input.hasOrigin()) {
ReleaseSupport.exec(ctx.gitRoot(), ctx.log(),
"git", "push", "origin", "v" + input.releaseVersion());
tagPushed = true;
ReleaseSupport.exec(ctx.gitRoot(), ctx.log(),
"git", "push", "origin", "main");
mainPushed = true;
createGitHubRelease(input.projectId(), input.releaseVersion(),
input.foundationUpgrades());
githubReleaseAttempted = true;
} else {
ctx.log().info("No 'origin' remote — skipping push");
ctx.log().info("No 'origin' remote — skipping GitHub Release");
}
return new FinalizeOutcome(tagPushed, mainPushed, githubReleaseAttempted);
}
/**
* Creates the GitHub Release for {@code v<version>} with
* milestone-based release notes, then closes the milestone and
* removes the {@code pending-release} label from any issues
* resolved in this release range.
*
* <p>Looks for a milestone named {@code <projectId> v<version>}
* in the configured issue repository ({@code ctx.request().issueRepo()}).
* If found, formats its closed issues as the GitHub Release body.
* Falls back to GitHub's auto-generated commit-based notes when no
* milestone exists.
*
* <p>All three steps (release create, milestone close, label
* cleanup) are best-effort once we reach this point: the Nexus
* deploy already shipped the artifact, so any failure here is
* logged as a warning with a manual-retry command rather than
* thrown as an exception.
*
* @param projectId the project's Maven artifact id
* @param version the released version (no {@code -SNAPSHOT})
* @param foundationUpgrades the upstream-version bumps this release applied;
* rendered as a "Foundation upgrades" section so a
* cascade-only rebuild's notes are never empty (#706)
*/
private void createGitHubRelease(String projectId, String version,
java.util.List<network.ike.plugin.CascadeBump> foundationUpgrades) {
File gitRoot = ctx.gitRoot();
String issueRepo = ctx.request().issueRepo();
String milestoneName = projectId + " v" + version;
// Close the issues this release's commits resolved (Fixes/Closes/
// Resolves trailers) BEFORE notes generation, so milestone notes
// reflect what shipped. GitHub can't auto-close cross-repo
// trailers, and IKE issues live in a separate tracker repo — this
// redeems the trailer contract so fixed issues don't dangle open
// (IKE-Network/ike-issues#799). Best-effort like the steps below.
try {
ReleaseNotesSupport.closeReferencedIssues(
gitRoot, null, "v" + version, issueRepo, ctx.log());
} catch (Exception e) {
ctx.log().warn("Could not close referenced issues "
+ "(release succeeded): " + e.getMessage());
}
Path notesFile;
try {
// When the milestone and foundation upgrades supply nothing,
// fall back to the commit-message changelog (v<version> against
// its previous tag) so a standalone, un-milestoned release still
// describes itself instead of degrading to GitHub's bare
// auto-notes (IKE-Network/ike-issues#775).
notesFile = ReleaseNotesSupport.generateToFile(
issueRepo, milestoneName, foundationUpgrades,
gitRoot, "v" + version, ctx.log());
} catch (Exception e) {
// A GitHub API failure here (rate limit, auth, network) must not
// fail a release whose artifacts have already shipped — fall back
// to GitHub's auto-generated notes (IKE-Network/ike-issues#572).
ctx.log().warn("Could not generate milestone notes for \""
+ milestoneName + "\" — falling back to auto-generated "
+ "notes: " + e.getMessage());
notesFile = null;
}
try {
if (notesFile != null) {
// Source (milestone / foundation upgrades / commit changelog)
// is logged by ReleaseNotesSupport; keep this generic.
ctx.log().info("Release notes generated for: " + milestoneName);
ReleaseSupport.exec(gitRoot, ctx.log(),
"gh", "release", "create", "v" + version,
"--title", version,
"--notes-file", notesFile.toString(),
"--verify-tag");
} else {
ctx.log().info("No milestone \"" + milestoneName
+ "\" found — using auto-generated notes");
ReleaseSupport.exec(gitRoot, ctx.log(),
"gh", "release", "create", "v" + version,
"--title", version,
"--generate-notes", "--verify-tag");
}
} catch (Exception e) {
ctx.log().warn("GitHub Release creation failed "
+ "(gh CLI may not be installed): " + e.getMessage());
ctx.log().warn("Run manually: gh release create v" + version
+ " --title " + version + " --generate-notes");
}
if (notesFile != null) {
try {
ReleaseNotesSupport.closeMilestone(issueRepo, milestoneName, ctx.log());
} catch (Exception e) {
ctx.log().warn("Could not close milestone (release succeeded): "
+ e.getMessage());
ctx.log().warn("Close manually: gh api repos/" + issueRepo
+ "/milestones/1 -X PATCH -f state=closed");
}
}
try {
ReleaseNotesSupport.removePendingReleaseLabels(
gitRoot, null, "v" + version, issueRepo, ctx.log());
} catch (Exception e) {
ctx.log().warn("Could not remove pending-release labels "
+ "(release succeeded): " + e.getMessage());
}
}
}