WorktreeGuard.java
package network.ike.plugin.release;
import network.ike.plugin.ReleaseSupport;
/**
* AutoCloseable wrapper around the release pipeline's worktree
* detach/restore boundary.
*
* <p>Acquired via {@link #detach(ReleaseContext, String, Runnable)} —
* the factory runs {@code git checkout <releaseTag>}, leaving the
* worktree pointed at the release tag. {@link #close()} stashes any
* foreign worktree changes (delegated to the caller-supplied
* {@code Runnable}) and then runs {@code git checkout main} to
* restore the worktree.
*
* <p>Use as a try-with-resources around the externally-visible
* deploy portion of the release pipeline:
*
* <pre>
* try (WorktreeGuard guard = WorktreeGuard.detach(ctx, "v" + version,
* () -> stashForeignWorktreeChanges(ctx, version))) {
* // site generation, gh-pages publish, Nexus/Central deploys
* }
* // worktree is restored to main, regardless of how the block exited
* </pre>
*
* <p>If the detach itself fails, the factory throws and no guard
* instance is returned — the caller's try-with-resources block
* never enters and no cleanup runs, matching the prior behavior
* where a failed detach left no work to undo.
*
* <p>Carved out of {@code ReleaseDraftMojo} during the Phase 4 P3
* prep commit (IKE-Network/ike-issues#489). The stash callback
* preserves the existing {@code stashForeignWorktreeChanges} as-is
* on the mojo so other code paths that invoke it are unchanged;
* a future commit can migrate the stash logic into a sibling
* helper once {@code LocalPhase} is extracted.
*/
public final class WorktreeGuard implements AutoCloseable {
private final ReleaseContext ctx;
private final Runnable foreignStash;
private WorktreeGuard(ReleaseContext ctx, Runnable foreignStash) {
this.ctx = ctx;
this.foreignStash = foreignStash;
}
/**
* Detaches the worktree to the given release tag.
*
* <p>Runs {@code git checkout <releaseTag>} from
* {@code ctx.gitRoot()}. If the checkout fails, this method
* throws and no guard instance is returned.
*
* @param ctx the release context carrying the git root and logger
* @param releaseTag the tag to detach to (typically {@code "v" + version})
* @param foreignStash callback invoked from {@link #close()} before the
* {@code git checkout main} step; intended to stash
* foreign mid-flight worktree changes that would
* otherwise block the checkout
* @return a new {@code WorktreeGuard} ready to be closed
*/
public static WorktreeGuard detach(ReleaseContext ctx, String releaseTag, Runnable foreignStash) {
ReleaseSupport.exec(ctx.gitRoot(), ctx.log(), "git", "checkout", releaseTag);
return new WorktreeGuard(ctx, foreignStash);
}
/**
* Restores the worktree to {@code main}.
*
* <p>Runs the {@code foreignStash} callback to clear any
* mid-flight worktree changes, then runs {@code git checkout main}.
* Called automatically when the try-with-resources block exits,
* regardless of whether the block exited normally or by exception.
*/
@Override
public void close() {
foreignStash.run();
ReleaseSupport.exec(ctx.gitRoot(), ctx.log(), "git", "checkout", "main");
}
}