feat(release): group the release note by type, then by scope
Sections are now the commit types (features, fixes, ...) with one sub-section per scope, marked with a label icon instead of the puzzle piece. ReleaseNotesTest, AGENTS.md and the release profile's comment follow the new order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xpLSeYKKHX6o16jzYLgZv
This commit is contained in:
@@ -42,7 +42,7 @@ Native installers with a bundled JRE are produced by jDeploy, driven from two fi
|
||||
* **Releasing.** `./mvnw -Prelease validate` (profile `release` in `pom.xml`, steps in `tools/release/Release.java`):
|
||||
bumps the version to `YYYY.M.N` (year and month without leading zero; `N` restarts at 0 each month), runs a
|
||||
fresh `./mvnw verify`, writes `distrib/release_note/release-note-<version>.md` (Conventional Commits since
|
||||
the previous `v<version>` tag — last 30 commits for the first release — grouped by scope then type,
|
||||
the previous `v<version>` tag — last 30 commits for the first release — grouped by type then scope,
|
||||
duplicate first lines merged), creates GitHub release `v<version>` in `Imag-In/Pholio` described by that
|
||||
note, with `pholio-<version>.jar`, its `.sha256` and the note as assets (via `gh`), commits `pom.xml` and
|
||||
tags `v<version>` locally, then commits and pushes the note from `distrib/`. Needs a clean working tree (or
|
||||
|
||||
@@ -787,8 +787,8 @@
|
||||
so this run - not the one executing the profile - is the one that sees the new
|
||||
version (manifest Implementation-Version, `pholio -V`).
|
||||
3. notes - distrib/release_note/release-note-<version>.md: the Conventional Commits since the
|
||||
previous v<version> tag (last 30 commits for the first release), grouped by scope
|
||||
then type, duplicate first lines merged.
|
||||
previous v<version> tag (last 30 commits for the first release), grouped by type
|
||||
then scope, duplicate first lines merged.
|
||||
4. publish - GitHub release v<version> in ${release.github.repo}, described by that note, with
|
||||
pholio-<version>.jar, its .sha256 and the note as assets, through the gh CLI (must
|
||||
be logged in). Only the tag and the assets go there: that repository's main branch
|
||||
|
||||
@@ -19,7 +19,7 @@ class ReleaseNotesTest {
|
||||
private static final Path SCRIPT = Path.of("tools", "release", "Release.java");
|
||||
|
||||
@Test
|
||||
void groupsConventionalCommitsByScopeThenTypeAndMergesDuplicates() throws Exception {
|
||||
void groupsConventionalCommitsByTypeThenScopeAndMergesDuplicates() throws Exception {
|
||||
String note = render("v2026.9.0", List.of(
|
||||
"feat(gallery): add a justified grid",
|
||||
"fix(gallery): keep the selection on resize",
|
||||
@@ -38,13 +38,12 @@ class ReleaseNotesTest {
|
||||
.contains("- keep the selection on resize _(×2)_");
|
||||
softly.assertThat(countOf(note, "add a justified grid")).isEqualTo(1);
|
||||
softly.assertThat(note).as("non-conventional subjects dropped").doesNotContain("Merge branch").doesNotContain("wip");
|
||||
softly.assertThat(note.indexOf("## 🧩 gallery")).as("scopes alphabetical")
|
||||
.isLessThan(note.indexOf("## 🧩 search"));
|
||||
softly.assertThat(note.indexOf("## 🧩 search")).as("unscoped last")
|
||||
.isLessThan(note.indexOf("## 🧩 general"));
|
||||
softly.assertThat(note.indexOf("### ✨ Features")).as("feat before fix within a scope")
|
||||
.isLessThan(note.indexOf("### 🐛 Bug fixes"));
|
||||
softly.assertThat(note).contains("### 📝 Documentation").contains("- 💥 **BREAKING** drop the legacy launcher");
|
||||
softly.assertThat(note.indexOf("## ✨ Features")).as("types first, feat before fix")
|
||||
.isLessThan(note.indexOf("## 🐛 Bug fixes"));
|
||||
softly.assertThat(note.indexOf("## 🐛 Bug fixes")).as("then fix before refactor")
|
||||
.isLessThan(note.indexOf("## ♻️ Refactoring"));
|
||||
softly.assertThat(note).as("scopes under their type").contains("## 📝 Documentation\n\n### 🏷️ search\n")
|
||||
.contains("## ♻️ Refactoring\n\n### 🏷️ general\n\n- 💥 **BREAKING** drop the legacy launcher");
|
||||
softly.assertAll();
|
||||
}
|
||||
|
||||
|
||||
+12
-12
@@ -32,7 +32,7 @@ import java.util.regex.Pattern;
|
||||
* source that was built) unless {@code allowDirty} is {@code true}, then rewrites the project version;
|
||||
* <li>{@code notes <pom> <notesDir>} — writes {@code release-note-<version>.md} into {@code notesDir}:
|
||||
* every Conventional Commit since the previous {@code v<version>} tag (the last
|
||||
* {@value #FIRST_RELEASE_COMMITS} commits when there is none yet), grouped by scope then by type — see
|
||||
* {@value #FIRST_RELEASE_COMMITS} commits when there is none yet), grouped by type then by scope — see
|
||||
* {@link #renderNotes};
|
||||
* <li>{@code publish <pom> <jar> <repo> <notesDir>} — creates GitHub release {@code v<version>} in
|
||||
* {@code repo}, described by that release note, with the jar (renamed {@code pholio-<version>.jar}),
|
||||
@@ -190,9 +190,9 @@ public final class Release {
|
||||
/**
|
||||
* The Markdown release note for {@code subjects} (commit first lines, newest first). Only Conventional
|
||||
* Commits count; the rest are dropped. Commits sharing the exact same first line are merged into one
|
||||
* entry, suffixed with how many there were. Entries are grouped by scope — alphabetically, unscoped
|
||||
* ones last under {@value #GENERAL_SCOPE} — then by type in {@link #TYPE_HEADINGS} order, unknown types
|
||||
* last; a breaking change ({@code !}) is flagged as such.
|
||||
* entry, suffixed with how many there were. Entries are grouped by type — in {@link #TYPE_HEADINGS}
|
||||
* order, unknown types last — then by scope, alphabetically with unscoped ones last under
|
||||
* {@value #GENERAL_SCOPE}; a breaking change ({@code !}) is flagged as such.
|
||||
*/
|
||||
static String renderNotes(String version, LocalDate date, String previousTag, List<String> subjects) {
|
||||
record Change(String type, String scope, String description, boolean breaking) {}
|
||||
@@ -214,13 +214,13 @@ public final class Release {
|
||||
List<String> typeOrder = List.copyOf(TYPE_HEADINGS.keySet());
|
||||
Comparator<String> byType = Comparator.<String>comparingInt(type -> typeOrder.contains(type) ? typeOrder.indexOf(type) : typeOrder.size())
|
||||
.thenComparing(Comparator.naturalOrder());
|
||||
Map<String, Map<String, List<String>>> grouped = new TreeMap<>(scopeOrder);
|
||||
Map<String, Map<String, List<String>>> grouped = new TreeMap<>(byType);
|
||||
changes.forEach((subject, change) -> {
|
||||
String line = (change.breaking() ? "💥 **BREAKING** " : "") + change.description()
|
||||
+ (counts.get(subject) > 1 ? " _(×" + counts.get(subject) + ")_" : "");
|
||||
grouped.computeIfAbsent(change.scope(), scope -> new TreeMap<>(byType))
|
||||
// doc and docs share one heading, so one section.
|
||||
.computeIfAbsent("doc".equals(change.type()) ? "docs" : change.type(), type -> new ArrayList<>())
|
||||
// doc and docs share one heading, so one section.
|
||||
grouped.computeIfAbsent("doc".equals(change.type()) ? "docs" : change.type(), type -> new TreeMap<>(scopeOrder))
|
||||
.computeIfAbsent(change.scope(), scope -> new ArrayList<>())
|
||||
.add(line);
|
||||
});
|
||||
|
||||
@@ -230,10 +230,10 @@ public final class Release {
|
||||
if (grouped.isEmpty()) {
|
||||
note.append("\nNo notable change.\n");
|
||||
}
|
||||
grouped.forEach((scope, byTypeLines) -> {
|
||||
note.append("\n## 🧩 ").append(scope).append("\n");
|
||||
byTypeLines.forEach((type, lines) -> {
|
||||
note.append("\n### ").append(TYPE_HEADINGS.getOrDefault(type, "🔹 " + type)).append("\n\n");
|
||||
grouped.forEach((type, byScopeLines) -> {
|
||||
note.append("\n## ").append(TYPE_HEADINGS.getOrDefault(type, "🔹 " + type)).append("\n");
|
||||
byScopeLines.forEach((scope, lines) -> {
|
||||
note.append("\n### 🏷️ ").append(scope).append("\n\n");
|
||||
lines.forEach(line -> note.append("- ").append(line).append("\n"));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user