Class ReleaseSupport

java.lang.Object
network.ike.plugin.ReleaseSupport

public class ReleaseSupport extends Object
Shared utilities for release mojos.

All subprocess invocations use ProcessBuilder — no library dependencies beyond the JDK and maven-plugin-api.

  • Nested Class Summary

    Nested Classes
    Modifier and Type
    Class
    Description
    static final record 
    A command paired with a display label for parallel execution.
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final String
    Base path on the site server.
    static final String
    SSH host alias used by wagon-ssh-external.
  • Method Summary

    Modifier and Type
    Method
    Description
    static List<File>
    bakeAliasIndirections(File gitRoot, org.apache.maven.api.plugin.Log log)
    Bake __ALIAS-driven indirections into all POM files under the git root.
    static String
    Convert a git branch name to a safe site path segment.
    static void
    cleanRemoteSiteDir(File workDir, org.apache.maven.api.plugin.Log log, String remotePath)
    Remove a directory tree on the site server via SSH.
    static void
    cleanRemoteSiteDir(File workDir, org.apache.maven.api.plugin.Log log, String remotePath, String... sshPrefix)
    Overload accepting an explicit SSH command prefix — package-private for testing against containers.
    static void
    copyDirectory(Path source, Path target)
    Recursively copy a directory tree.
    static void
    Recursively copy a directory tree, excluding top-level subdirectories whose names look like release versions.
    static String
    Get the current git branch name.
    static void
    Recursively delete a directory and all its contents.
    static String
    deriveCheckpointVersion(String pomVersion, File gitRoot)
    Derive a checkpoint version from the current POM version.
    static String
    deriveNextSnapshot(String releaseVersion)
    Derive the next SNAPSHOT version by incrementing the last numeric segment.
    static String
    deriveReleaseVersion(String snapshotVersion)
    Derive the release version from a SNAPSHOT version.
    static Path
    detectAggregatedStaging(Path source, String projectId)
    Detect aggregated-reactor staging where the project's own site lives alongside multiple sibling subdirs (ike-issues#351).
    static Path
    detectHttpsUrlStaging(Path source, String projectId)
    Detect URL-as-path staging where site:stage has mapped an https://-form <site><url> to a literal directory tree under target/staging/ (ike-issues#359).
    static Path
    Detect parent-artifactId staging nesting (ike-issues#342).
    static void
    exec(File workDir, org.apache.maven.api.plugin.Log log, String... command)
    Run a command, inherit IO so output streams to the Maven console.
    static String
    execCapture(File workDir, String... command)
    Run a command and capture stdout as a trimmed String.
    static String
    execCaptureAndLog(File workDir, org.apache.maven.api.plugin.Log log, String... command)
    Run a command, streaming output through Maven's logger AND capturing the full output as a String.
    static void
    execParallel(File workDir, org.apache.maven.api.plugin.Log log, ReleaseSupport.LabeledTask... tasks)
    Run multiple commands concurrently, prefixing each line of output with the task's label (e.g., [nexus] ...).
    static List<File>
    findPomFiles(File gitRoot)
    Find all pom.xml files under the git root, excluding target/ directories and the .mvn/ directory.
    static List<Path>
    findSubmoduleSiteDirs(Path layer, Path exclude)
    Find one-level-deep subdirectories under layer that look like rendered Maven-site outputs (have an index.html at the top), excluding exclude itself (the workspace's own subtree, already published at the gh-pages root).
    static String
    getRemoteUrl(File workDir, String remoteName)
    Return the URL of a named git remote, or null if the remote does not exist.
    static void
    gitAddFiles(File gitRoot, org.apache.maven.api.plugin.Log log, List<File> files)
    Stage a list of files with git add.
    static File
    gitRoot(File startDir)
    Get the git repository root directory.
    static boolean
    hasRemote(File workDir, String remoteName)
    Check whether a named git remote exists.
    static boolean
    Check whether a directory has no entries.
    static boolean
    Check if the current platform is macOS.
    static boolean
    Check if the current platform is Windows.
    static void
    publishProjectSiteToGhPages(Path stagingDir, String repoUrl, org.apache.maven.api.plugin.Log log, String projectId, String version)
    Publish a project's rendered site to its repo's gh-pages branch using the hybrid structure (ike-issues#312, #332).
    static String
    Read the project's own <artifactId> from a POM file, skipping any <artifactId> inside the <parent> block.
    static String
    Read the <description> element from a POM file.
    static String
    Read the project's own <groupId> from a POM file, skipping any <groupId> inside the <parent> block.
    static String
    readPomName(File pomFile)
    Read the <name> element from a POM file.
    static String
    readPomProperty(File pomFile, String propertyName)
    Read a <properties> value from a POM file by element name.
    static String
    Read the project's own <version> from a POM file, skipping any <version> inside the <parent> block.
    static List<File>
    replaceProjectVersionRefs(File gitRoot, String version, org.apache.maven.api.plugin.Log log)
    Replace all occurrences of ${project.version} with a literal version string in every POM file under the git root.
    static void
    Assert that the git working tree is clean (no staged or unstaged changes).
    static File
    resolveMavenWrapper(File gitRoot, org.apache.maven.api.plugin.Log log)
    Resolve the Maven executable.
    static List<File>
    restoreBackups(File gitRoot, org.apache.maven.api.plugin.Log log)
    Restore all POM files from their .ike-backup copies and delete the backup files.
    static void
    routeSubprocessLine(org.apache.maven.api.plugin.Log log, String line)
    Route a subprocess output line through Maven's logger at the correct level.
    static void
    routeSubprocessLine(org.apache.maven.api.plugin.Log log, String line, String prefix)
    Route a subprocess output line through Maven's logger with a prefix.
    static void
    setPomVersion(File pomFile, String oldVersion, String newVersion)
    Replace the project's own <version>old</version> with <version>new</version>, skipping any version inside the <parent> block.
    static String
    siteDiskPath(String projectId, String siteType, String subPath)
    Resolve the on-disk site path for a given project, type, and optional subdirectory.
    static String
    Return the staging path for a site deploy (final path + ".staging").
    static String
    Return the scpexe URL for the staging directory.
    static void
    stampOutputTimestamp(File pomFile, String newTimestamp, org.apache.maven.api.plugin.Log log)
    Stamp <project.build.outputTimestamp> in the root POM to newTimestamp, enabling reproducible builds for the release.
    static void
    swapRemoteSiteDir(File workDir, org.apache.maven.api.plugin.Log log, String remotePath)
    Atomically swap a newly deployed site into place on the server.
    static void
    swapRemoteSiteDir(File workDir, org.apache.maven.api.plugin.Log log, String remotePath, String... sshPrefix)
    Overload accepting an explicit SSH command prefix — package-private for testing against containers.
    static boolean
    tagExists(File gitRoot, String tagName)
    Check whether a git tag exists (locally).
    static List<File>
    unbakeAliasIndirections(File gitRoot, org.apache.maven.api.plugin.Log log)
    Unbake __ALIAS-driven indirections from all POM files under the git root — the inverse of bakeAliasIndirections(File, Log).
    static void
    updateLatestSymlink(File workDir, org.apache.maven.api.plugin.Log log, String remotePath)
    Update the latest symlink alongside a version-prefixed site deploy so that <site-base>/latest/ always points at the most recent release (ike-issues#303).
    static void
    updateLatestSymlink(File workDir, org.apache.maven.api.plugin.Log log, String remotePath, String... sshPrefix)
    Overload accepting an explicit SSH command prefix — package-private for testing against containers.
    static String
    updateVersionProperty(String pomContent, String propertyName, String newVersion)
    Update a named Maven property in POM content.
    static void
    Validate that a remote path is safe for deletion operations.

    Methods inherited from class Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

  • Method Details

    • isMacOS

      public static boolean isMacOS()
      Check if the current platform is macOS.
      Returns:
      true if running on macOS or Darwin
    • exec

      public static void exec(File workDir, org.apache.maven.api.plugin.Log log, String... command) throws org.apache.maven.api.plugin.MojoException
      Run a command, inherit IO so output streams to the Maven console. Throws on non-zero exit code.
      Parameters:
      workDir - working directory for the subprocess
      log - Maven logger for output routing
      command - the command and arguments to execute
      Throws:
      org.apache.maven.api.plugin.MojoException - if the command exits non-zero or cannot be started
    • routeSubprocessLine

      public static void routeSubprocessLine(org.apache.maven.api.plugin.Log log, String line)
      Route a subprocess output line through Maven's logger at the correct level. Strips Maven log prefixes ([INFO], [WARNING], [ERROR]) from the line to avoid redundant nesting.
      Parameters:
      log - Maven logger
      line - raw subprocess output line
    • routeSubprocessLine

      public static void routeSubprocessLine(org.apache.maven.api.plugin.Log log, String line, String prefix)
      Route a subprocess output line through Maven's logger with a prefix.

      Recognized prefixes are stripped and routed to the matching Maven log level. Unrecognized lines are routed to info so that subprocess activity (especially git operations during release) is visible in the build log.

      Specific git error patterns (fatal:, error:, remote: error:, ! [rejected], ! [remote rejected]) are detected explicitly and routed to error so they cannot be missed in the log. Earlier behavior routed all unprefixed output to debug, which silently hid gh-pages push failures (see IKE-Network/ike-issues#329).

      Parameters:
      log - Maven logger
      line - raw subprocess output line
      prefix - string prepended to each routed line
    • execParallel

      public static void execParallel(File workDir, org.apache.maven.api.plugin.Log log, ReleaseSupport.LabeledTask... tasks) throws org.apache.maven.api.plugin.MojoException
      Run multiple commands concurrently, prefixing each line of output with the task's label (e.g., [nexus] ...).

      Spawns virtual threads to read stdout/stderr from each process. All processes run to completion even if one fails — the exception reports which task(s) failed.

      Parameters:
      workDir - working directory for each subprocess
      log - Maven logger for output routing
      tasks - the labeled tasks to run concurrently
      Throws:
      org.apache.maven.api.plugin.MojoException - if any task fails or execution is interrupted
    • execCapture

      public static String execCapture(File workDir, String... command) throws org.apache.maven.api.plugin.MojoException
      Run a command and capture stdout as a trimmed String. Throws on non-zero exit code.
      Parameters:
      workDir - working directory for the subprocess
      command - the command and arguments to execute
      Returns:
      trimmed stdout output
      Throws:
      org.apache.maven.api.plugin.MojoException - if the command exits non-zero or cannot be started
    • execCaptureAndLog

      public static String execCaptureAndLog(File workDir, org.apache.maven.api.plugin.Log log, String... command) throws org.apache.maven.api.plugin.MojoException
      Run a command, streaming output through Maven's logger AND capturing the full output as a String. Throws on non-zero exit.
      Parameters:
      workDir - working directory for the subprocess
      log - Maven logger for real-time output
      command - the command and arguments to execute
      Returns:
      the complete stdout+stderr output as a trimmed string
      Throws:
      org.apache.maven.api.plugin.MojoException - if the command exits non-zero
    • readPomVersion

      public static String readPomVersion(File pomFile) throws org.apache.maven.api.plugin.MojoException
      Read the project's own <version> from a POM file, skipping any <version> inside the <parent> block.
      Parameters:
      pomFile - the POM file to read
      Returns:
      the version string
      Throws:
      org.apache.maven.api.plugin.MojoException - if the file cannot be read or has no version
    • stampOutputTimestamp

      public static void stampOutputTimestamp(File pomFile, String newTimestamp, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Stamp <project.build.outputTimestamp> in the root POM to newTimestamp, enabling reproducible builds for the release.

      The property must already exist in the POM (inherited from ike-parent). If it is absent this method is a no-op with a warning.

      Parameters:
      pomFile - the root POM to update
      newTimestamp - ISO-8601 UTC timestamp, e.g. 2026-03-30T12:00:00Z
      log - Maven log (used for warnings only)
      Throws:
      org.apache.maven.api.plugin.MojoException - if the file cannot be read or written
    • setPomVersion

      public static void setPomVersion(File pomFile, String oldVersion, String newVersion) throws org.apache.maven.api.plugin.MojoException
      Replace the project's own <version>old</version> with <version>new</version>, skipping any version inside the <parent> block.
      Parameters:
      pomFile - the POM file to update
      oldVersion - the current version string to replace
      newVersion - the new version string
      Throws:
      org.apache.maven.api.plugin.MojoException - if the version is not found or the file cannot be updated
    • isWindows

      public static boolean isWindows()
      Check if the current platform is Windows.
      Returns:
      true if os.name contains "win"
    • resolveMavenWrapper

      public static File resolveMavenWrapper(File gitRoot, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Resolve the Maven executable. Prefers the Maven wrapper (mvnw on Unix, mvnw.cmd on Windows) at the git root; falls back to system Maven located via which on Unix or where on Windows.
      Parameters:
      gitRoot - the git repository root directory
      log - Maven logger
      Returns:
      the resolved Maven executable
      Throws:
      org.apache.maven.api.plugin.MojoException - if neither wrapper nor system Maven is found
    • gitRoot

      public static File gitRoot(File startDir) throws org.apache.maven.api.plugin.MojoException
      Get the git repository root directory.
      Parameters:
      startDir - any directory inside the repository
      Returns:
      the repository root directory
      Throws:
      org.apache.maven.api.plugin.MojoException - if git rev-parse fails
    • requireCleanWorktree

      public static void requireCleanWorktree(File workDir) throws org.apache.maven.api.plugin.MojoException
      Assert that the git working tree is clean (no staged or unstaged changes).
      Parameters:
      workDir - any directory inside the repository
      Throws:
      org.apache.maven.api.plugin.MojoException - if the working tree has uncommitted changes
    • currentBranch

      public static String currentBranch(File workDir) throws org.apache.maven.api.plugin.MojoException
      Get the current git branch name.
      Parameters:
      workDir - any directory inside the repository
      Returns:
      the current branch name
      Throws:
      org.apache.maven.api.plugin.MojoException - if git rev-parse fails
    • hasRemote

      public static boolean hasRemote(File workDir, String remoteName)
      Check whether a named git remote exists.
      Parameters:
      workDir - any directory inside the repository
      remoteName - the remote name to check (e.g., "origin")
      Returns:
      true if the remote exists
    • getRemoteUrl

      public static String getRemoteUrl(File workDir, String remoteName)
      Return the URL of a named git remote, or null if the remote does not exist.
      Parameters:
      workDir - any directory inside the repository
      remoteName - the remote name (typically "origin")
      Returns:
      the remote URL, or null if the remote is absent
    • deriveReleaseVersion

      public static String deriveReleaseVersion(String snapshotVersion)
      Derive the release version from a SNAPSHOT version. "2-SNAPSHOT" becomes "2"; "1.1.0-SNAPSHOT" becomes "1.1.0".
      Parameters:
      snapshotVersion - the SNAPSHOT version string
      Returns:
      the release version without the -SNAPSHOT suffix
    • deriveNextSnapshot

      public static String deriveNextSnapshot(String releaseVersion)
      Derive the next SNAPSHOT version by incrementing the last numeric segment. "2" becomes "3-SNAPSHOT"; "1.1.0" becomes "1.1.1-SNAPSHOT".
      Parameters:
      releaseVersion - the release version to increment
      Returns:
      the next SNAPSHOT version
    • updateVersionProperty

      public static String updateVersionProperty(String pomContent, String propertyName, String newVersion)
      Update a named Maven property in POM content. Replaces <propertyName>oldValue</propertyName> with <propertyName>newVersion</propertyName>.
      Parameters:
      pomContent - the POM file content as a string
      propertyName - the Maven property name (e.g., "ike-bom.version")
      newVersion - the new version value
      Returns:
      the updated POM content (unchanged if property not found)
    • findPomFiles

      public static List<File> findPomFiles(File gitRoot) throws org.apache.maven.api.plugin.MojoException
      Find all pom.xml files under the git root, excluding target/ directories and the .mvn/ directory.
      Parameters:
      gitRoot - the git repository root directory
      Returns:
      list of discovered POM files
      Throws:
      org.apache.maven.api.plugin.MojoException - if the file tree cannot be walked
    • replaceProjectVersionRefs

      public static List<File> replaceProjectVersionRefs(File gitRoot, String version, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Replace all occurrences of ${project.version} with a literal version string in every POM file under the git root. Before replacing, each affected file is saved as pom.xml.ike-backup so it can be restored later.
      Parameters:
      gitRoot - the git repository root directory
      version - the literal version to substitute
      log - Maven logger
      Returns:
      the list of POM files that were modified
      Throws:
      org.apache.maven.api.plugin.MojoException - if a file cannot be read or written
    • restoreBackups

      public static List<File> restoreBackups(File gitRoot, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Restore all POM files from their .ike-backup copies and delete the backup files. This reverses replaceProjectVersionRefs(File, String, Log).
      Parameters:
      gitRoot - the git repository root directory
      log - Maven logger
      Returns:
      the list of POM files that were restored
      Throws:
      org.apache.maven.api.plugin.MojoException - if a backup cannot be restored
    • bakeAliasIndirections

      public static List<File> bakeAliasIndirections(File gitRoot, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Bake __ALIAS-driven indirections into all POM files under the git root. For each property whose name ends in __ALIAS (carrying a comma-separated list of legacy short-name aliases), generates and adds a corresponding indirection property <short>${G__GA__A__VERSION}</short> to the same POM — unless the property is already declared.

      This is the release-time materialization step: source poms declare the alias relationship via __ALIAS metadata only; the actual indirection lines (required for Maven property resolution) are added by release-publish to the tagged source pom, then removed by unbakeAliasIndirections(File, Log) after the tag. See IKE-Network/ike-issues#527.

      Parameters:
      gitRoot - the git repository root directory
      log - Maven logger
      Returns:
      the list of POM files that were modified
      Throws:
      org.apache.maven.api.plugin.MojoException - if a file cannot be read or written
    • unbakeAliasIndirections

      public static List<File> unbakeAliasIndirections(File gitRoot, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Unbake __ALIAS-driven indirections from all POM files under the git root — the inverse of bakeAliasIndirections(File, Log). For each property whose name ends in __ALIAS, removes any corresponding indirection property whose value exactly matches the expected canonical reference ${G__GA__A__VERSION}. Indirection lines with different values (e.g. project-local hand-written ones) are left alone.

      This is the post-tag removal step: after the release tag captures the baked state, the source pom is restored to its declarative-only form for the SNAPSHOT cycle. See IKE-Network/ike-issues#527.

      Parameters:
      gitRoot - the git repository root directory
      log - Maven logger
      Returns:
      the list of POM files that were modified
      Throws:
      org.apache.maven.api.plugin.MojoException - if a file cannot be read or written
    • gitAddFiles

      public static void gitAddFiles(File gitRoot, org.apache.maven.api.plugin.Log log, List<File> files) throws org.apache.maven.api.plugin.MojoException
      Stage a list of files with git add.
      Parameters:
      gitRoot - the git repository root directory
      log - Maven logger
      files - the files to stage
      Throws:
      org.apache.maven.api.plugin.MojoException - if the git add command fails
    • deriveCheckpointVersion

      public static String deriveCheckpointVersion(String pomVersion, File gitRoot) throws org.apache.maven.api.plugin.MojoException
      Derive a checkpoint version from the current POM version.

      Format: {base}-checkpoint.{yyyyMMdd}.{shortSha} where base is the POM version minus -SNAPSHOT, and shortSha is the abbreviated SHA of the current HEAD commit.

      This scheme is fully deterministic — the same commit on any machine always produces the same version string. No tag-sequence coordination across machines is required.

      Parameters:
      pomVersion - current POM version (may include -SNAPSHOT)
      gitRoot - git repository root (for HEAD SHA lookup)
      Returns:
      the checkpoint version string
      Throws:
      org.apache.maven.api.plugin.MojoException - if the HEAD SHA cannot be resolved
    • tagExists

      public static boolean tagExists(File gitRoot, String tagName)
      Check whether a git tag exists (locally).
      Parameters:
      gitRoot - the git repository root directory
      tagName - the tag name to check
      Returns:
      true if the tag exists locally
    • cleanRemoteSiteDir

      public static void cleanRemoteSiteDir(File workDir, org.apache.maven.api.plugin.Log log, String remotePath) throws org.apache.maven.api.plugin.MojoException
      Remove a directory tree on the site server via SSH.

      Used to clean up snapshot sites after release or feature-finish.

      Safety: validates the path starts with SITE_DISK_BASE and contains at least two path components after the base to prevent accidental deletion of the entire site root.

      Parameters:
      workDir - local directory for process execution
      log - Maven log
      remotePath - absolute path on the server (e.g., /srv/ike-site/ike-pipeline/snapshot/main)
      Throws:
      org.apache.maven.api.plugin.MojoException - if the path is unsafe or SSH fails
    • cleanRemoteSiteDir

      public static void cleanRemoteSiteDir(File workDir, org.apache.maven.api.plugin.Log log, String remotePath, String... sshPrefix) throws org.apache.maven.api.plugin.MojoException
      Overload accepting an explicit SSH command prefix — package-private for testing against containers.
      Parameters:
      workDir - local directory for process execution
      log - Maven log
      remotePath - absolute path on the server to remove
      sshPrefix - the SSH command tokens (e.g., "ssh", "-i", "key", "-p", "2222", "user@localhost")
      Throws:
      org.apache.maven.api.plugin.MojoException - if the path is unsafe or SSH fails
    • swapRemoteSiteDir

      public static void swapRemoteSiteDir(File workDir, org.apache.maven.api.plugin.Log log, String remotePath) throws org.apache.maven.api.plugin.MojoException
      Atomically swap a newly deployed site into place on the server.

      The deployment flow is:

      1. SCP deploys to a staging path (<target>.staging)
      2. This method renames the old directory to <target>.old
      3. Renames the staging directory to the final target
      4. Removes the old directory

      This avoids a window where the site is missing (rm + deploy) and ensures the site always serves either the old or new version.

      Parameters:
      workDir - local directory for process execution
      log - Maven log
      remotePath - final target path on the server
      Throws:
      org.apache.maven.api.plugin.MojoException - if SSH commands fail
    • swapRemoteSiteDir

      public static void swapRemoteSiteDir(File workDir, org.apache.maven.api.plugin.Log log, String remotePath, String... sshPrefix) throws org.apache.maven.api.plugin.MojoException
      Overload accepting an explicit SSH command prefix — package-private for testing against containers.
      Parameters:
      workDir - local directory for process execution
      log - Maven log
      remotePath - final target path on the server
      sshPrefix - the SSH command tokens (e.g., "ssh", "-i", "key", "-p", "2222", "user@localhost")
      Throws:
      org.apache.maven.api.plugin.MojoException - if the path is unsafe or SSH fails
    • siteStagingPath

      public static String siteStagingPath(String diskPath)
      Return the staging path for a site deploy (final path + ".staging").
      Parameters:
      diskPath - the final on-disk site path
      Returns:
      diskPath with .staging appended
    • updateLatestSymlink

      public static void updateLatestSymlink(File workDir, org.apache.maven.api.plugin.Log log, String remotePath) throws org.apache.maven.api.plugin.MojoException
      Update the latest symlink alongside a version-prefixed site deploy so that <site-base>/latest/ always points at the most recent release (ike-issues#303).

      For a release deployed to /srv/ike-site/ike-platform/17/, this issues cd /srv/ike-site/ike-platform && ln -snf 17 latest on the site host. Idempotent — the -f flag replaces any prior symlink target.

      Uses the same SSH host as swapRemoteSiteDir(File, Log, String). Best-effort: callers should catch MojoException and surface as a warning rather than failing the release — the version-prefixed site is reachable at its own URL even if the alias update fails.

      Parameters:
      workDir - local directory for process execution
      log - Maven log
      remotePath - the version-prefixed final site path (e.g. /srv/ike-site/ike-platform/17)
      Throws:
      org.apache.maven.api.plugin.MojoException - if the path is unsafe or SSH fails
    • updateLatestSymlink

      public static void updateLatestSymlink(File workDir, org.apache.maven.api.plugin.Log log, String remotePath, String... sshPrefix) throws org.apache.maven.api.plugin.MojoException
      Overload accepting an explicit SSH command prefix — package-private for testing against containers.
      Parameters:
      workDir - local directory for process execution
      log - Maven log
      remotePath - the version-prefixed final site path
      sshPrefix - the SSH command tokens
      Throws:
      org.apache.maven.api.plugin.MojoException - if the path is unsafe or SSH fails
    • siteStagingUrl

      public static String siteStagingUrl(String targetUrl)
      Return the scpexe URL for the staging directory.
      Parameters:
      targetUrl - the final site URL
      Returns:
      targetUrl with .staging appended
    • publishProjectSiteToGhPages

      public static void publishProjectSiteToGhPages(Path stagingDir, String repoUrl, org.apache.maven.api.plugin.Log log, String projectId, String version) throws org.apache.maven.api.plugin.MojoException
      Publish a project's rendered site to its repo's gh-pages branch using the hybrid structure (ike-issues#312, #332).

      Layout produced after each release:

      • / — the just-released version's site at the root (so https://ike.network/<repo>/ serves the current release, the same as before).
      • /<version>/ — the just-released version preserved under a versioned subdirectory for citations and reproducibility.
      • /latest/ — a copy of the just-released version under the canonical "latest" path. Not a git symlink: GitHub Pages doesn't follow them reliably.
      • Earlier /<version>/ subdirectories from prior releases are preserved unchanged.

      Mechanics: the existing gh-pages branch is cloned (preserving full history including all prior <version>/ subdirs); root-level files and non-version root subdirs are wiped (stale assets from the previous release shouldn't linger); staging is copied to root, to <version>/, and to latest/; an additive commit is pushed (no --force). On first-time publish (no gh-pages branch yet) the bootstrap path uses git checkout --orphan and force-push.

      Adds a .nojekyll marker so GitHub Pages skips Jekyll processing — the content is already rendered HTML and we don't want underscore-prefixed directories to be stripped.

      Does NOT write a CNAME file: the org-level IKE-Network.github.io/CNAME (set to ike.network) extends to all project pages under the org automatically. A per-project CNAME would either be ignored or conflict.

      Patterned on OrgSiteSupport.publishToGhPages (in the ike-maven-plugin module) but generalized to any project's staging dir + remote. ike-workspace-model can't @link directly to ike-maven-plugin classes — it sits below in the dependency stack, so they're not on its javadoc classpath.

      Parameters:
      stagingDir - directory containing the rendered site (typically target/staging/)
      repoUrl - git URL of the project repo to push to
      log - Maven logger
      projectId - project artifact ID, used in the commit message
      version - release version, used in the commit message
      Throws:
      org.apache.maven.api.plugin.MojoException - if any step fails
    • validateRemotePath

      public static void validateRemotePath(String remotePath) throws org.apache.maven.api.plugin.MojoException
      Validate that a remote path is safe for deletion operations.

      Ensures the path starts with SITE_DISK_BASE and has sufficient depth to prevent accidental deletion of the site root.

      Parameters:
      remotePath - absolute path on the server
      Throws:
      org.apache.maven.api.plugin.MojoException - if the path is unsafe
    • siteDiskPath

      public static String siteDiskPath(String projectId, String siteType, String subPath)
      Resolve the on-disk site path for a given project, type, and optional subdirectory.
      Parameters:
      projectId - Maven artifact ID (e.g., "ike-pipeline")
      siteType - "release", "snapshot", or "checkpoint"
      subPath - optional subdirectory (branch name, version); null or blank to omit
      Returns:
      absolute path on the server
    • branchToSitePath

      public static String branchToSitePath(String branch)
      Convert a git branch name to a safe site path segment. Replaces / with / (keeps hierarchy for feature/name structure).
      Parameters:
      branch - git branch name
      Returns:
      sanitized path segment safe for use in URLs and file paths
    • readPomGroupId

      public static String readPomGroupId(File pomFile) throws org.apache.maven.api.plugin.MojoException
      Read the project's own <groupId> from a POM file, skipping any <groupId> inside the <parent> block.

      Used by cascade self-identification (IKE-Network/ike-issues#402): a reactor-root POM declares its own <groupId>, which the release goals match against release-cascade.yaml entries.

      Parameters:
      pomFile - the POM file to read
      Returns:
      the group ID string
      Throws:
      org.apache.maven.api.plugin.MojoException - if the file cannot be read or declares no own group ID
    • readPomArtifactId

      public static String readPomArtifactId(File pomFile) throws org.apache.maven.api.plugin.MojoException
      Read the project's own <artifactId> from a POM file, skipping any <artifactId> inside the <parent> block.
      Parameters:
      pomFile - the POM file to read
      Returns:
      the artifact ID string
      Throws:
      org.apache.maven.api.plugin.MojoException - if the file cannot be read or has no artifact ID
    • readPomProperty

      public static String readPomProperty(File pomFile, String propertyName) throws org.apache.maven.api.plugin.MojoException
      Read a <properties> value from a POM file by element name.

      Used by cascade resolution (IKE-Network/ike-issues#404) to read ike-tooling.version — the version at which a foundation repo resolves the ike-build-standards cascade artifact.

      Parameters:
      pomFile - the POM file to read
      propertyName - the property element name (e.g. ike-tooling.version)
      Returns:
      the property value, or null if the POM declares no such property
      Throws:
      org.apache.maven.api.plugin.MojoException - if the file cannot be read
    • isEmptyDirectory

      public static boolean isEmptyDirectory(Path dir) throws org.apache.maven.api.plugin.MojoException
      Check whether a directory has no entries. Returns true if the path is a directory with zero entries; false if it has at least one entry. Throws if the path is not a readable directory.

      Used by the gh-pages publish flow (ike-issues#334) to distinguish "directory exists but is empty" (e.g., mvn site:stage produced nothing for a single-module project) from "directory exists and has content" (the normal case).

      Parameters:
      dir - the directory to inspect
      Returns:
      true if the directory contains no entries
      Throws:
      org.apache.maven.api.plugin.MojoException - if the directory cannot be listed
    • detectParentArtifactNesting

      public static Path detectParentArtifactNesting(Path source, String projectId) throws org.apache.maven.api.plugin.MojoException
      Detect parent-artifactId staging nesting (ike-issues#342).

      When a Maven project has a <parent> block, maven-site-plugin's site:stage writes content to target/staging/<parentArtifactId>/<projectId>/ rather than target/staging/. The hybrid gh-pages publish path treats stagingDir as the source of truth, so the published tree ends up double-nested at /<repo>/<version>/<parentArtifactId>/<projectId>/ when it should be at /<repo>/<version>/.

      This helper detects that wrap by checking:

      1. source contains exactly one entry that is itself a directory, and
      2. that single directory contains a non-empty subdirectory whose name equals projectId.

      Returns the path to <single-entry>/<projectId> when both conditions hold; otherwise returns null to indicate no unwrap is needed.

      The check is intentionally conservative — it requires a single top-level entry so it does not mis-fire on staging trees that legitimately have multiple top-level dirs (the normal aggregated-reactor case where each module's site lives under its own subdirectory).

      Parameters:
      source - the post-#337-unwrap effective staging source
      projectId - the project's own artifactId
      Returns:
      the unwrapped path, or null if no nesting was detected
      Throws:
      org.apache.maven.api.plugin.MojoException - if the directory cannot be inspected
    • detectAggregatedStaging

      public static Path detectAggregatedStaging(Path source, String projectId) throws org.apache.maven.api.plugin.MojoException
      Detect aggregated-reactor staging where the project's own site lives alongside multiple sibling subdirs (ike-issues#351).

      When a workspace pom inherits a <site> URL with no common ancestor among its reactor subprojects' URLs (e.g. each module gets https://ike.network/<artifactId>/), site:stage flattens each module's site at its own top-level path under target/staging/. The workspace's own content lives in staging/<projectId>/ and the subprojects are siblings. Without unwrap, the gh-pages root gets the whole flattened tree — wrong index.html, missing the workspace's own pages.

      Detection criteria (all must hold):

      1. source contains at least one top-level entry, AND
      2. a subdirectory named projectId exists at the top level, AND
      3. that subdirectory is non-empty.

      Single-top-level cases are handled by detectParentArtifactNesting(Path, String); this method's value-add is the multi-top-level case where projectId is one of several siblings.

      Returns the path to source/<projectId> when the detection fires; null otherwise.

      Parameters:
      source - the post-#337/#342-unwrap effective staging source
      projectId - the project's own artifactId
      Returns:
      the unwrapped path, or null
      Throws:
      org.apache.maven.api.plugin.MojoException - if the directory cannot be inspected
    • detectHttpsUrlStaging

      public static Path detectHttpsUrlStaging(Path source, String projectId) throws org.apache.maven.api.plugin.MojoException
      Detect URL-as-path staging where site:stage has mapped an https://-form <site><url> to a literal directory tree under target/staging/ (ike-issues#359).

      For a <site><url>https://ike.network/<projectId>/ inside a multi-module reactor, maven-site-plugin produces target/staging/https:/ike.network/<projectId>/<content> — scheme (https:), host (ike.network), and each path segment each become a real subdirectory. The pre-#304 scpexe URLs also went through this transformation, but their path segments aligned with where site-deploy actually wrote, so the layout was sensible. Post-#304 the https:// URLs survive only for relative-path computation and the URL-as-path staging is wasted — the actual publish target is gh-pages.

      Detection: descend into source/https:/ (or source/http:/), then a single host directory, then the project's own <projectId>/ subdirectory. When all three exist and <projectId>/ is non-empty, return it as the effective staging source. The caller then applies the remaining unwraps (version-nested #337, parent-artifactId #342, aggregated #351) to the narrowed source — e.g. ike-platform's https://ike.network/ike-platform/${project.version}/ resolves to source/https:/ike.network/ike-platform/ here, then #337 descends into 40/.

      Returns null if any layer of the expected structure is missing or the project's own directory is empty — staying conservative so non-matching projects (single-module reactors, scpexe-URL projects, etc.) fall through to the existing unwrap chain unchanged.

      Parameters:
      source - the staging directory to inspect
      projectId - the project's artifactId
      Returns:
      the unwrap target, or null if no URL-as-path nesting was detected
      Throws:
      org.apache.maven.api.plugin.MojoException - if a directory cannot be listed
    • findSubmoduleSiteDirs

      public static List<Path> findSubmoduleSiteDirs(Path layer, Path exclude) throws org.apache.maven.api.plugin.MojoException
      Find one-level-deep subdirectories under layer that look like rendered Maven-site outputs (have an index.html at the top), excluding exclude itself (the workspace's own subtree, already published at the gh-pages root).

      Used by publishProjectSiteToGhPages(Path, String, Log, String, String) step (4) to surface sibling submodule subtrees in a multi-module reactor. The conservative index.html-presence check keeps the walk from mis-classifying resource dirs (css/, fonts/, images/) or version dirs as submodule sites.

      Returns an empty list if layer is not a directory or has no matching entries. Order is unspecified.

      Parameters:
      layer - the directory to scan
      exclude - a path that, when matched against an entry, causes the entry to be skipped (typically the workspace's own narrowed staging source)
      Returns:
      list of submodule site directories
      Throws:
      org.apache.maven.api.plugin.MojoException - if the directory cannot be listed
    • deleteDirectory

      public static void deleteDirectory(Path dir)
      Recursively delete a directory and all its contents. Best-effort — failures are silently ignored.
      Parameters:
      dir - the directory to delete
    • copyDirectory

      public static void copyDirectory(Path source, Path target) throws IOException
      Recursively copy a directory tree.
      Parameters:
      source - the source directory to copy from
      target - the target directory to copy to
      Throws:
      IOException - if a file cannot be copied
    • copyDirectoryExcludingTopLevelVersionDirs

      public static void copyDirectoryExcludingTopLevelVersionDirs(Path source, Path target) throws IOException
      Recursively copy a directory tree, excluding top-level subdirectories whose names look like release versions. Files at the top level and non-version subdirectories at the top level are copied normally. Inside any non-filtered subdirectory, all entries are copied without further filtering — the exclusion applies only at depth 0.

      Used by publishProjectSiteToGhPages(Path, String, Log, String, String) to keep the gh-pages source clean of staging-pollution. Three causes of top-level version-prefixed entries in the source:

      1. Stale local mirrors: when a project's site.deploy.url contains the version segment and maven-clean-plugin preserves target/staging/, prior releases' <version>/ subdirs accumulate there.
      2. Self-nesting: copying staging that already has a <currentVersion>/ subdir into a version subdir would produce <currentVersion>/<currentVersion>/....
      3. Race with the wipe step: the wipe preserved version dirs in the gh-pages clone; if staging happens to also contain those names, the copy would clobber the preserved content with potentially stale mirror copies.

      The filter applies the same digit-prefix heuristic as isVersionDirName(String): top-level dirs whose names start with a digit are skipped.

      Parameters:
      source - the source directory to copy from
      target - the target directory to copy to
      Throws:
      IOException - if a file cannot be copied
      See Also:
    • readPomName

      public static String readPomName(File pomFile) throws org.apache.maven.api.plugin.MojoException
      Read the <name> element from a POM file.
      Parameters:
      pomFile - the POM file to read
      Returns:
      the name, or null if not present
      Throws:
      org.apache.maven.api.plugin.MojoException - if the file cannot be read
    • readPomDescription

      public static String readPomDescription(File pomFile) throws org.apache.maven.api.plugin.MojoException
      Read the <description> element from a POM file.
      Parameters:
      pomFile - the POM file to read
      Returns:
      the description, or null if not present
      Throws:
      org.apache.maven.api.plugin.MojoException - if the file cannot be read