Class ReleaseNotesSupport

java.lang.Object
network.ike.plugin.ReleaseNotesSupport

public final class ReleaseNotesSupport extends Object
Generates release notes from a GitHub milestone's closed issues.

Queries the GitHub REST API, categorizes issues by label into Fixes, Enhancements, and Internal sections, and produces markdown. JSON responses are parsed via SnakeYAML (JSON is valid YAML).

Used by both ws:release-notes (standalone) and ike:release (integrated into the release workflow).

  • Method Details

    • generate

      public static String generate(String repo, String milestone, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Generate release notes markdown for a named milestone.
      Parameters:
      repo - GitHub repository in owner/repo format
      milestone - milestone title (e.g., "ike-tooling v57")
      log - Maven logger (may be null for non-Maven callers)
      Returns:
      formatted markdown, or null if the milestone is not found
      Throws:
      org.apache.maven.api.plugin.MojoException - if the GitHub API call fails
    • generate

      public static String generate(String repo, String milestone, List<CascadeBump> upgrades, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Generate release notes markdown for a named milestone, including a "Foundation upgrades" section for any cascade version bumps the release applied (IKE-Network/ike-issues#706).

      Returns null only when there is genuinely nothing to report — no milestone and no foundation upgrades — so the caller can fall back to GitHub's auto-generated notes. A cascade-only rebuild (no milestone, but real upstream bumps) yields a non-null body announcing what it was rebuilt against, rather than a silent "no changes."

      Parameters:
      repo - GitHub repository in owner/repo format
      milestone - milestone title (e.g., "ike-docs v76")
      upgrades - the upstream-version bumps the release applied (may be empty)
      log - Maven logger (may be null for non-Maven callers)
      Returns:
      formatted markdown, or null if there is nothing to report
      Throws:
      org.apache.maven.api.plugin.MojoException - if the GitHub API call fails
    • generateToFile

      public static Path generateToFile(String repo, String milestone, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Try to generate release notes, writing to a temp file suitable for gh release create --notes-file. Returns the path, or null if notes could not be generated.
      Parameters:
      repo - GitHub repository in owner/repo format
      milestone - milestone title
      log - Maven logger (may be null)
      Returns:
      path to the temp file, or null on failure
      Throws:
      org.apache.maven.api.plugin.MojoException - if the GitHub API call fails
    • generateToFile

      public static Path generateToFile(String repo, String milestone, List<CascadeBump> upgrades, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Try to generate release notes (with a "Foundation upgrades" section sourced from upgrades, #706), writing to a temp file suitable for gh release create --notes-file.
      Parameters:
      repo - GitHub repository in owner/repo format
      milestone - milestone title
      upgrades - the upstream-version bumps the release applied (may be empty)
      log - Maven logger (may be null)
      Returns:
      path to the temp file, or null if there was nothing to report
      Throws:
      org.apache.maven.api.plugin.MojoException - if the GitHub API call fails
    • generateToFile

      public static Path generateToFile(String repo, String milestone, List<CascadeBump> upgrades, File gitDir, String toRef, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Like generateToFile(String, String, List, Log), but when the milestone and foundation upgrades yield nothing to report, falls back to the commit-message changelog for previousTag..toRef instead of returning null — so a standalone, un-milestoned release still describes itself from its own commits rather than degrading to GitHub's bare auto-generated notes (IKE-Network/ike-issues#775).

      Curated milestone notes still take precedence: the changelog is consulted only when generate(String, String, Log) reports nothing. Machinery commits (release:, post-release:, merges) are filtered by formatChangelog(List). If the changelog is also empty (a genuine first release, or an unreadable shallow range) this returns null and the caller falls back to gh --generate-notes as before.

      Parameters:
      repo - GitHub repository in owner/repo format
      milestone - milestone title (also the changelog heading)
      upgrades - the upstream-version bumps the release applied (may be empty)
      gitDir - the release repo's git root, for the changelog fallback
      toRef - the release ref/tag the changelog runs up to (e.g. v117)
      log - Maven logger (may be null)
      Returns:
      path to the notes temp file, or null if there is genuinely nothing to report
      Throws:
      org.apache.maven.api.plugin.MojoException - if the GitHub API call fails
    • closeMilestone

      public static boolean closeMilestone(String repo, String milestone, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Close a GitHub milestone by title. Warns if the milestone has open issues remaining.
      Parameters:
      repo - GitHub repository in owner/repo format
      milestone - milestone title
      log - Maven logger
      Returns:
      true if closed, false if not found
      Throws:
      org.apache.maven.api.plugin.MojoException - if the GitHub API call fails
    • snapshotMilestone

      public static ReleaseNotesSupport.TestingContext snapshotMilestone(String repo, String milestone, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Fetch all issues (open and closed) for a milestone, returning them categorized for a checkpoint testing context snapshot.
      Parameters:
      repo - GitHub repository in owner/repo format
      milestone - milestone title
      log - Maven logger
      Returns:
      snapshot with closed (ready to test) and open (in progress) issues, or null if milestone not found
      Throws:
      org.apache.maven.api.plugin.MojoException - if the GitHub API call fails
    • removePendingReleaseLabels

      public static int removePendingReleaseLabels(File gitDir, String previousTag, String headRef, String fallbackRepo, org.apache.maven.api.plugin.Log log)
      Remove the pending-release label from every issue referenced by a release-closing trailer (Fixes, Closes, Resolves and grammatical variants) in commits between previousTag and headRef.

      Implements the "label = live state" half of the pending-release pattern defined in IKE-COMMITS.md: a commit lands marking an issue Fixes …, the issue gets the pending-release label as a not-yet-shipped marker, and when the release actually ships the label comes off so is:closed label:pending-release accurately reflects fixes still awaiting a release.

      Trailer references must use the full <owner>/<repo>#N form; bare #N references are resolved against fallbackRepo.

      Pass null for previousTag to auto-derive it via git describe --tags --abbrev=0 <headRef>^. If no previous tag is reachable, label removal is skipped with an informational message.

      Non-fatal: any failure (missing gh CLI, missing label, network error, auth error) is logged and the method continues processing the remaining references. The release is already done at this point.

      Parameters:
      gitDir - the git working tree
      previousTag - the previous release tag, or null to auto-derive
      headRef - the new release commit or tag (e.g., "v57")
      fallbackRepo - owner/repo for bare #N refs; may be null to ignore bare refs
      log - Maven logger (may be null)
      Returns:
      number of issues from which the label was actually removed
    • commitMessagesBetween

      public static List<String> commitMessagesBetween(File gitDir, String fromRef, String toRef)
      The full commit messages (subject + body) in fromRef..toRef, newest first — the input formatChangelog(List) consumes.

      Reads via git log, so it works on the release worktree (a full checkout). Returns an empty list if the range can't be read (e.g. a shallow checkout, or no previous tag).

      Parameters:
      gitDir - the git working tree
      fromRef - the exclusive lower bound (e.g. the previous tag)
      toRef - the inclusive upper bound (e.g. HEAD or a tag)
      Returns:
      the commit messages, newest first
    • hasAnyIssueTrailer

      public static boolean hasAnyIssueTrailer(String commitMessage)
      Returns true if commitMessage contains at least one IKE-COMMITS.md issue trailer (Fixes, Closes, Resolves, Refs and grammatical variants) with a #N or <owner>/<repo>#N reference.

      Used by release-time preflight to flag commits that violate the "every commit references a tracked issue" rule.

      Parameters:
      commitMessage - commit message body, including subject and trailers
      Returns:
      true if any issue trailer is present
    • parseClosingTrailers

      public static Set<ReleaseNotesSupport.IssueRef> parseClosingTrailers(String commitMessages, String fallbackRepo)
      Parse closing-keyword trailers (e.g., Fixes, Closes, Resolves and grammatical variants) from a block of commit message text. Returns unique references in encounter order.

      Trailers without an explicit owner/repo prefix are resolved against fallbackRepo; if fallbackRepo is null, bare references are ignored.

      Public so the workspace plugin (in a different module) can call this from ws:checkpoint-publish per IKE-Network/ike-issues#394 — checkpoint reporting needs the same trailer parser that release-time label removal uses.

      Parameters:
      commitMessages - concatenated commit message bodies
      fallbackRepo - owner/repo for bare references, or null
      Returns:
      ordered set of unique issue references found
    • closeReferencedIssues

      public static int closeReferencedIssues(File gitDir, String previousTag, String headRef, String fallbackRepo, org.apache.maven.api.plugin.Log log)
      Close every open issue referenced by a release-closing trailer (Fixes, Closes, Resolves and grammatical variants) in commits between previousTag and headRef.

      GitHub's native Fixes #N auto-close only fires when the issue lives in the commit's own repository. IKE centralizes issues in a separate tracker repo, so cross-repo trailers never auto-close — this redeems that trailer contract at release time so fixed issues don't dangle open (IKE-Network/ike-issues#799). Call it before milestone-notes generation so the notes reflect what shipped.

      Idempotent (issues already closed are skipped) and non-fatal: the artifact has already deployed at this point, so any failure is logged and the remaining references are still processed. Trailer references use the full <owner>/<repo>#N form; bare #N references resolve against fallbackRepo.

      Parameters:
      gitDir - the git working tree
      previousTag - the previous release tag, or null to auto-derive
      headRef - the new release commit or tag (e.g., "v57")
      fallbackRepo - owner/repo for bare #N refs, or null
      log - Maven logger (may be null)
      Returns:
      number of issues actually closed
    • generateFullHistory

      public static Path generateFullHistory(String repo, Path outputDir, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Generate a full release history page as AsciiDoc, covering all closed milestones and any closed issues without a milestone. Each milestone becomes a section with categorized issues.
      Parameters:
      repo - GitHub repository in owner/repo format
      outputDir - directory to write release-notes.adoc into
      log - Maven logger
      Returns:
      the written file path, or null on failure
      Throws:
      org.apache.maven.api.plugin.MojoException - if the GitHub API call fails
    • generateFullHistoryXhtml

      public static Path generateFullHistoryXhtml(String repo, Path outputDir, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Generate a full release history as an XHTML fragment suitable for inclusion in the Maven site via generatedSiteDirectory. The fragment is wrapped in a root <div> with an <h1> title, matching the format that maven-site-plugin expects from generated content.
      Parameters:
      repo - GitHub repository in owner/repo format
      outputDir - directory to write release-notes.xhtml into
      log - Maven logger
      Returns:
      the written file path, or null on failure
      Throws:
      org.apache.maven.api.plugin.MojoException - if the GitHub API call fails
    • generateAsciidocToFile

      public static Path generateAsciidocToFile(String repo, String milestone, Path outputDir, org.apache.maven.api.plugin.Log log) throws org.apache.maven.api.plugin.MojoException
      Generate release notes as AsciiDoc and write to a file, suitable for inclusion in the Maven site build. Returns the path, or null if the milestone is not found.
      Parameters:
      repo - GitHub repository in owner/repo format
      milestone - milestone title
      outputDir - directory to write the AsciiDoc file into
      log - Maven logger
      Returns:
      path to the written file, or null on failure
      Throws:
      org.apache.maven.api.plugin.MojoException - if the GitHub API call fails
    • formatAsciidoc

      public static String formatAsciidoc(String milestoneName, List<ReleaseNotesSupport.Issue> issues, String repo)
      Format release notes as AsciiDoc for site integration.
      Parameters:
      milestoneName - the milestone title
      issues - closed issues from the milestone
      repo - GitHub repository (e.g., "IKE-Network/ike-issues")
      Returns:
      AsciiDoc-formatted release notes
    • formatNotes

      public static String formatNotes(String milestoneName, List<ReleaseNotesSupport.Issue> issues)
      Format release notes as Markdown for GitHub Release bodies.
      Parameters:
      milestoneName - the milestone title
      issues - closed issues from the milestone
      Returns:
      Markdown-formatted release notes
    • formatNotes

      public static String formatNotes(String milestoneName, List<ReleaseNotesSupport.Issue> issues, List<CascadeBump> upgrades)
      Format release notes as Markdown, including a "Foundation upgrades" section for cascade version bumps (IKE-Network/ike-issues#706).
      Parameters:
      milestoneName - the milestone title
      issues - closed issues from the milestone
      upgrades - the upstream-version bumps the release applied (may be empty)
      Returns:
      Markdown-formatted release notes
    • isMachineryCommit

      public static boolean isMachineryCommit(String subject)
      Whether a commit subject is release machinery (a cadence commit a release itself produces), and so should be filtered from a changelog.
      Parameters:
      subject - the commit subject line
      Returns:
      true for release/merge/post-release/bump machinery
    • parseIssueRefs

      public static List<String> parseIssueRefs(String commitMessage)
      Extract every issue reference from a commit message's trailers, in display form, de-duplicated and in first-seen order.

      The full owner/repo#N form is preserved verbatim (so a consumer — a Zulip linkifier, a GitHub release body — links it in whatever repo it lives, with no tracker assumed). A bare #N reference is returned as #N: its repository is ambiguous and is deliberately not guessed.

      Parameters:
      commitMessage - the full commit message (subject + body)
      Returns:
      the referenced issues in display form, e.g. ["IKE-Network/ike-issues#705", "ikmdev/komet-desktop#12"]
    • formatChangelog

      public static String formatChangelog(List<String> commitMessages)
      Compose a "What's changed" changelog from a list of commit messages: one bullet per substantive commit (release machinery filtered out), each annotated with the full-form issue references parsed from its trailers.

      Repo-agnostic: no issue tracker is assumed or hardcoded — each reference carries its own owner/repo from the commit trailer (IKE-COMMITS.md mandates the full form for exactly this cross-repo reason). A trailing bare-#N parenthetical on the subject is stripped so refs aren't shown twice.

      Returns the empty string when nothing substantive remains — the caller omits the whole "What's changed" block in that case.

      Parameters:
      commitMessages - full commit messages (subject + body) in display order (typically newest-first from a git log/compare range)
      Returns:
      the Markdown bullet list, or "" if empty
    • formatWorkspaceChangelog

      public static String formatWorkspaceChangelog(Manifest from, Manifest to, ReleaseNotesSupport.SubprojectCommits commits)
      Compose a per-subproject "What's changed" changelog for a workspace checkpoint by diffing each subproject's workspace.yaml pin between two checkpoints (IKE-Network/ike-issues#792).

      This is the workspace-aware counterpart to the single-repo formatChangelog(List) path. Run at an aggregator checkpoint it sees every subproject's code change, not just the aggregator's own workspace.yaml/merge commits — the gap that made the checkpoint Zulip note omit subproject changes while the GitHub release body (computed per-subproject from the pins) showed them.

      For each subproject in to whose pinned sha differs from its from pin, the supplied ReleaseNotesSupport.SubprojectCommits yields that subproject's commit messages, which formatChangelog(List) filters and formats into a ### <name> section with a GitHub compare link. Subprojects with an unchanged pin, no prior pin needed for a link, or no substantive commits are handled gracefully; a subproject contributing no substantive commits is omitted entirely.

      Parameters:
      from - the previous checkpoint's manifest, or null to treat every subproject as new
      to - the current checkpoint's manifest
      commits - supplies each changed subproject's commit messages
      Returns:
      the per-subproject Markdown (subprojects in manifest order), or "" when nothing substantive changed
    • formatWorkspaceChangelog

      public static String formatWorkspaceChangelog(File aggregatorGitDir, String fromRef, String toRef)
      Git-backed formatWorkspaceChangelog(Manifest, Manifest, SubprojectCommits): reads workspace.yaml from the aggregator at fromRef and toRef, and reads each changed subproject's commits from its worktree under the aggregator root. A subproject with no present worktree (or no prior pin) contributes no bullets and is therefore omitted.

      Returns "" when toRef has no workspace.yaml (not an aggregator), so the caller can fall back to the single-repo changelog.

      Parameters:
      aggregatorGitDir - the workspace aggregator git working tree
      fromRef - the previous checkpoint ref/tag
      toRef - the current ref (a checkpoint tag or HEAD)
      Returns:
      the per-subproject Markdown, or "" when unavailable
    • cascadeTopicLabel

      public static String cascadeTopicLabel(Instant now, ZoneId zone)
      The human label for a cascade topic — the creating build's date in its own zone, with the zone abbreviation appended so it is unambiguous across the operators' time zones (e.g. "2026-06-19 PDT").

      Computed once, by the build that creates the topic; never recomputed by other builds (they find the open topic instead), so there is no need for a midnight-safe rollover.

      Parameters:
      now - the creating build's instant
      zone - the release server's zone (e.g. ZoneId.systemDefault())
      Returns:
      the label, e.g. "2026-06-19 PDT"
    • inProgressCascadeTopic

      public static String inProgressCascadeTopic(String label)
      The open-cascade topic name for a label, e.g. "2026-06-19 PDT Release Cascade — in progress". The "— in progress" suffix is the correlation key: at most one such topic exists at a time (releases run serially and completion renames it away), so a build finds the cascade by matching it.
      Parameters:
      label - the cascadeTopicLabel(Instant, ZoneId) value
      Returns:
      the in-progress topic name
    • completedCascadeTopic

      public static String completedCascadeTopic(String label, String parentVersion)
      The completed-cascade topic name, carrying the ike-parent version the terminal release establishes, e.g. "2026-06-19 PDT ike-parent Release Cascade v66".
      Parameters:
      label - the cascadeTopicLabel(Instant, ZoneId) value
      parentVersion - the released ike-parent (ike-platform) version
      Returns:
      the completed topic name
    • cascadeLabelOf

      public static String cascadeLabelOf(String topic)
      Recover the cascadeTopicLabel(Instant, ZoneId) from an in-progress topic name, so the terminal build can derive the completed name from the topic it found, or null if topic is not an in-progress cascade topic.
      Parameters:
      topic - a Zulip topic name
      Returns:
      the label, or null if it isn't an in-progress cascade topic
    • cascadeMetaEnv

      public static String cascadeMetaEnv(String label)
      A shell-sourceable env snippet carrying the cascade label and in-progress topic name, so a notify step gets everything from one ike:release-changelog call without recomputing the date in shell. Values are single-quoted; safe because a label is only digits, hyphens, spaces, and a zone abbreviation — never a quote.
      CASCADE_LABEL='2026-06-19 PDT'
      CASCADE_TOPIC_INPROGRESS='2026-06-19 PDT Release Cascade — in progress'
      
      Parameters:
      label - the cascadeTopicLabel(Instant, ZoneId) value
      Returns:
      the env-file content (newline-terminated)
    • formatCascadeSummary

      public static String formatCascadeSummary(String parentVersion, List<ReleaseNotesSupport.CascadeMember> members)
      The cascade-completion summary the terminal build posts, naming the ike-parent version and listing every repo released in the cascade.
      Parameters:
      parentVersion - the released ike-parent (ike-platform) version
      members - the repos released, in cascade order
      Returns:
      the Markdown summary