diff --git a/.changeset/brave-otters-versioning.md b/.changeset/brave-otters-versioning.md new file mode 100644 index 0000000..8525a26 --- /dev/null +++ b/.changeset/brave-otters-versioning.md @@ -0,0 +1,7 @@ +--- +"changesets": minor +--- + +Add support for the `independent` version strategy, allowing sub-modules in a multi-module Maven project to be versioned independently of each other. + +The prepare→release handoff has moved from the single-line `.changeset/VERSION` file to a per-artifactId map in `.changeset/VERSIONS` (plural). The current version is now read directly from each module's `pom.xml` rather than from the version file. Any pre-existing `.changeset/VERSION` file is no longer consulted and can be safely deleted. diff --git a/README.md b/README.md index 35218fe..951a0d1 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,96 @@ users recognize the ways of working and feel at home. This is it, at the moment. Stay tuned for more docs later on, thanks! +## Versioning strategies + +By default every module in the Maven reactor shares one version (the **fixed** strategy) — any changeset bumps every module +to the same new version. This matches the typical Maven convention where child modules inherit `` from the parent. + +To opt into per-module versioning, drop a `.changeset/config.json` at the reactor root: + +```json +{ + "versioning": "independent", + "linked": [["module-a", "module-b"]], + "fixed": [["module-c", "module-d"]] +} +``` + +- **`fixed` (default)** — entire reactor bumps as one. +- **`independent`** — each Maven module tracks its own version. Optional `linked` and `fixed` arrays group some modules: + - **`linked` group**: members that have changesets bump together to the same new version; members without changesets stay where they are. The new version is the *highest current version in the group* bumped by the *highest level among changesets touching the group*. + - **`fixed` group**: every member of the group bumps together, even members without their own changesets. + +A changeset's frontmatter lists modules by `artifactId`: + +``` +--- +"module-a": minor +"module-b": patch +--- + +Description of the change. +``` + +The same `artifactId` cannot appear in more than one group; that is a config validation error. + +### Independent versioning and `` declarations + +For `independent` (or `linked` / `fixed` sub-groups) to actually update individual submodule versions, each Maven submodule +must declare its own `` in its `pom.xml`. Submodules that inherit `` from the parent only bump together +with the parent. + +## BOM (Bill of Materials) support + +If your reactor contains a BOM — a `pom`-packaged module whose `` pins sibling modules' versions via +`` (the Spring Boot convention) — opt in via `.changeset/config.json`: + +```json +{ + "versioning": "independent", + "bom": { + "module": "fortnox-spring-boot-dependencies", + "consumerParent": "fortnox-spring-boot-starter-parent" + } +} +``` + +Behavior: + +- **The BOM auto-bumps** at the max level of any tracked module's bump (any reactor module that pins through the BOM's + ``). Explicit changesets targeting the BOM still work — they combine with the synthesized level. +- **The BOM's ``** that pin sibling versions are rewritten on `prepare` (to the next `-SNAPSHOT`) and on + `release` (to the release version). The mapping is discovered by walking the BOM's ``; you don't + have to name properties by any convention. +- **`consumerParent`** (optional) is the module a consumer sets as their ``. It typically has no `` of its + own and inherits from the BOM via Maven parent inheritance. When set, it is *excluded* from the plan (no own bump) and is + used as the changelog header so consumers see entries named after the artifact they actually pin. Its `` + reference is updated when the BOM bumps. Validation: the artifactId must exist in the reactor, and if it declares its own + `` it must match the BOM's. +- **Changelog rendering** in BOM mode collapses to a single top-level release header, with one sub-section per bumped + module (including the BOM, which gets a synthesized `Pinned version updates` block listing the new sibling versions). + +### Releasing a starter without cutting a BOM release + +Pass `-DskipBom=true` to `changesets:prepare` to bypass BOM behavior for that invocation. The starters bump as in plain +independent mode, the BOM's pom is left untouched (version *and* pinned properties), and the changelog falls back to the +standard per-module sections. The `bom` block in `.changeset/config.json` stays in place — `skipBom` is a per-run override, +not a config change. Use it when you want to ship a quick starter patch between full BOM releases. + +## How `prepare` and `release` interact + +`changesets:prepare` (aggregator goal, runs once at the reactor root) reads all changesets in `.changeset/`, computes a new +version per affected module, writes the new release versions to `.changeset/VERSIONS` (a properties file keyed by +artifactId), updates each affected submodule's pom to the next `*-SNAPSHOT`, and prepends a release block to the root +`CHANGELOG.md`. + +`changesets:release` reads `.changeset/VERSIONS` and writes each module's pom to its release version. The `.changeset/VERSIONS` +file is the handoff between the two goals. + +> **Upgrading from an earlier version:** the previous single-file `.changeset/VERSION` (uppercase, no `S`) is no longer read +> or written. The current version is now taken from each module's `pom.xml` directly, and the prepare→release handoff lives +> in `.changeset/VERSIONS`. If you have a leftover `.changeset/VERSION` file, it is unused and safe to delete. + ## Dependency updates Due to the way automated dependency update bots like Dependabot and Renovate work, there is often a large influx of automated changesets that are not easy to merge into the normal changelog. They can also be the source of an unwanted amount of noise in the changelog. @@ -26,9 +116,24 @@ Dependencies that have been updated to new versions multiple times between relea ## Release Maven Plugin Integration -To delegate versioning to the Release Maven Plugin, you can use the `ChangesetsVersionPolicy` together with the `useReleasePluginIntegration` flag: +You can hand versioning off to the maven-release-plugin by wiring in `ChangesetsVersionPolicy`. -``` +### When to use it + +**Recommended:** fixed versioning (the default) — single-module or multi-module. Every module bumps together to one +version, one tag, one release commit. + +**Not recommended:** independent versioning. Maven-release-plugin's model is *release the whole reactor atomically* — +every reactor module gets a release version, a tag, and a next-dev bump, whether it was targeted by a changeset or not. +That fights the point of `independent`, where you only want to release modules that actually changed. For independent +versioning, use plain `changesets:prepare` + `changesets:release` (see [How `prepare` and `release` interact](#how-prepare-and-release-interact)) +and release-plugin is not recommended. + +### Setup + +Configure both plugins in the reactor POM: + +```xml @@ -36,7 +141,7 @@ To delegate versioning to the Release Maven Plugin, you can use the `ChangesetsV changesets-maven-plugin ${changesets.plugin.version} - true + true @@ -45,6 +150,7 @@ To delegate versioning to the Release Maven Plugin, you can use the `ChangesetsV changesets v@{project.version} + false @@ -55,6 +161,24 @@ To delegate versioning to the Release Maven Plugin, you can use the `ChangesetsV + +``` + +### Flow + +``` +mvn changesets:prepare +git add . && git commit -m "chore: prepare release" +mvn release:prepare release:perform ``` -Goals should then be invoked as `changesets:prepare release:prepare release:perform`. `changesets:release` should *not* be used. \ No newline at end of file +With `useReleasePluginIntegration=true`, `changesets:prepare` writes `.changeset/VERSIONS` and `CHANGELOG.md` but +does not touch any poms. `ChangesetsVersionPolicy` then reads `VERSIONS` and tells release-plugin the release + +next-dev version per module. All pom rewrites happen in release-plugin's own commit — one clean "release X" commit +in history instead of two. + +`changesets:release` is *not* used in this flow. + +If you'd rather have `changesets:prepare` update the poms itself (e.g. to inspect them before triggering the +release-plugin), omit `useReleasePluginIntegration`. The trade-off is an extra "chore: prepare release" commit +before `release:prepare` runs. \ No newline at end of file diff --git a/changesets-java/src/main/java/se/fortnox/changesets/BumpPlanner.java b/changesets-java/src/main/java/se/fortnox/changesets/BumpPlanner.java new file mode 100644 index 0000000..7d25e3f --- /dev/null +++ b/changesets-java/src/main/java/se/fortnox/changesets/BumpPlanner.java @@ -0,0 +1,223 @@ +package se.fortnox.changesets; + +import org.slf4j.Logger; +import se.fortnox.changesets.ChangesetsConfig.Bom; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.slf4j.LoggerFactory.getLogger; +import static se.fortnox.changesets.ChangesetsConfig.VersioningStrategy.FIXED; + +/** + * Resolves which modules should be bumped to which versions for a release, given the + * full set of changesets, the current reactor state, and the configured versioning strategy. + *

+ * Pure function: no I/O, no side effects. + */ +public class BumpPlanner { + private static final Logger LOG = getLogger(BumpPlanner.class); + + public record ModuleBump(String artifactId, String currentVersion, String newVersion, List changesets) { + public boolean isVersionChange() { + return !currentVersion.equals(newVersion); + } + } + + public static Map plan( + List changesets, + Map reactor, + ChangesetsConfig config + ) { + Set known = reactor.keySet(); + List known_changesets = filterKnownModules(changesets, known); + + List groups = buildGroups(reactor, config); + + Map result = new LinkedHashMap<>(); + for (Group group : groups) { + List groupChangesets = changesetsForGroup(known_changesets, group); + if (groupChangesets.isEmpty()) { + continue; + } + result.putAll(planGroup(group, groupChangesets, reactor)); + } + + if (config.bom() != null) { + applyBomPlan(result, known_changesets, reactor, config.bom()); + } + + return result; + } + + /** + * Applies BOM (Bill of Materials) semantics on top of the base plan: + *

    + *
  • Removes the consumer-parent from the plan (it inherits its version from the BOM).
  • + *
  • Synthesizes/merges a BOM bump at the max level of any tracked module bump, + * combined with any explicit BOM-targeted changesets.
  • + *
  • The BOM bump's {@code changesets} list contains only the explicit changesets + * targeting the BOM, so changelog rendering shows them naturally; the synthesized + * part is purely a version-bump signal.
  • + *
+ */ + private static void applyBomPlan( + Map result, + List allChangesets, + Map reactor, + Bom bom + ) { + String bomModule = bom.module(); + String consumerParent = bom.consumerParent(); + + if (consumerParent != null) { + result.remove(consumerParent); + } + + EnumSet trackedLevels = EnumSet.noneOf(Level.class); + for (ModuleBump bump : result.values()) { + if (bump.artifactId().equals(bomModule)) { + continue; + } + if (!bump.isVersionChange()) { + continue; + } + for (Changeset c : bump.changesets()) { + trackedLevels.add(c.level()); + } + } + + List bomExplicit = allChangesets.stream() + .filter(c -> c.packageName().equals(bomModule)) + .toList(); + + if (trackedLevels.isEmpty() && bomExplicit.isEmpty()) { + return; + } + + List combinedForVersionCalc = new ArrayList<>(bomExplicit); + for (Level level : trackedLevels) { + combinedForVersionCalc.add(new Changeset(bomModule, level, "", null)); + } + + String bomCurrent = reactor.get(bomModule); + if (bomCurrent == null) { + throw new IllegalArgumentException( + "bom.module '" + bomModule + "' is not present in the reactor"); + } + String bomNew = VersionCalculator.getNewVersion(bomCurrent, combinedForVersionCalc); + + result.put(bomModule, new ModuleBump(bomModule, bomCurrent, bomNew, bomExplicit)); + } + + private static List filterKnownModules(List changesets, Set known) { + List kept = new ArrayList<>(changesets.size()); + for (Changeset c : changesets) { + if (known.contains(c.packageName())) { + kept.add(c); + } else { + LOG.warn("Changeset {} references unknown module '{}', ignoring", + c.file() == null ? "" : c.file().getName(), + c.packageName()); + } + } + return kept; + } + + private static List buildGroups(Map reactor, ChangesetsConfig config) { + if (config.versioning() == FIXED) { + return List.of(new Group(GroupKind.FIXED, new LinkedHashSet<>(reactor.keySet()))); + } + + List groups = new ArrayList<>(); + Set assigned = new HashSet<>(); + for (List g : config.fixed()) { + groups.add(new Group(GroupKind.FIXED, new LinkedHashSet<>(g))); + assigned.addAll(g); + } + for (List g : config.linked()) { + groups.add(new Group(GroupKind.LINKED, new LinkedHashSet<>(g))); + assigned.addAll(g); + } + for (String artifactId : reactor.keySet()) { + if (!assigned.contains(artifactId)) { + groups.add(new Group(GroupKind.INDIVIDUAL, new LinkedHashSet<>(List.of(artifactId)))); + } + } + return groups; + } + + private static List changesetsForGroup(List changesets, Group group) { + return changesets.stream() + .filter(c -> group.members().contains(c.packageName())) + .toList(); + } + + private static Map planGroup(Group group, List groupChangesets, Map reactor) { + String baseVersion = highestVersion(group.members().stream() + .map(reactor::get) + .filter(java.util.Objects::nonNull) + .toList()); + String newVersion = VersionCalculator.getNewVersion(baseVersion, groupChangesets); + + Set activeMembers = groupChangesets.stream() + .map(Changeset::packageName) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + // Iterate in declaration order so changelog output is stable regardless of changeset file order + LinkedHashSet bumpMembers = new LinkedHashSet<>(); + for (String member : group.members()) { + boolean include = switch (group.kind()) { + case FIXED, INDIVIDUAL -> true; + case LINKED -> activeMembers.contains(member); + }; + if (include) { + bumpMembers.add(member); + } + } + + Map result = new LinkedHashMap<>(); + for (String member : bumpMembers) { + String currentVersion = reactor.get(member); + if (currentVersion == null) { + continue; + } + List memberChangesets = groupChangesets.stream() + .filter(c -> c.packageName().equals(member)) + .toList(); + result.put(member, new ModuleBump(member, currentVersion, newVersion, memberChangesets)); + } + return result; + } + + private static String highestVersion(Collection versions) { + String maxRaw = null; + org.semver4j.Semver maxSemver = null; + for (String v : versions) { + org.semver4j.Semver semver = Optional.ofNullable(org.semver4j.Semver.coerce(v)) + .map(org.semver4j.Semver::withClearedBuild) + .orElseThrow(() -> new IllegalArgumentException("Cannot coerce \"%s\" into a semantic version.".formatted(v))); + if (maxSemver == null || semver.compareTo(maxSemver) > 0) { + maxSemver = semver; + maxRaw = v; + } + } + if (maxRaw == null) { + throw new IllegalStateException("Group has no resolvable members in reactor"); + } + return maxRaw; + } + + private enum GroupKind { FIXED, LINKED, INDIVIDUAL } + + private record Group(GroupKind kind, Set members) {} +} diff --git a/changesets-java/src/main/java/se/fortnox/changesets/ChangelogAggregator.java b/changesets-java/src/main/java/se/fortnox/changesets/ChangelogAggregator.java index f7706ea..2e825a3 100644 --- a/changesets-java/src/main/java/se/fortnox/changesets/ChangelogAggregator.java +++ b/changesets-java/src/main/java/se/fortnox/changesets/ChangelogAggregator.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -78,8 +79,182 @@ public Path mergeChangesetsToChangelog(String packageName, String version) { return changelogFile; } + /** + * Merge a multi-module release into the root CHANGELOG.md: one section per module that + * bumped, ordered by the {@code moduleEntries} iteration order. Consumes (deletes) all + * passed-in changeset files. Modules with empty changeset lists are skipped. + * + * @param moduleEntries Ordered map of artifactId → (newVersion, changesets) for the release + * @return Path to the written CHANGELOG.md, or the changeset dir if nothing was written + */ + public Path mergeReleaseToChangelog(Map moduleEntries) { + return mergeReleaseToChangelog(moduleEntries, null); + } + + /** + * BOM-aware variant. When {@code bomContext} is non-null, emits a single top-level + * release header (consumer-parent + BOM version) with each bumped module rendered + * as a nested sub-section. The BOM's section additionally lists its pinned-version + * updates. + */ + public Path mergeReleaseToChangelog(Map moduleEntries, BomContext bomContext) { + Path changesetsDir = this.baseDir.resolve(CHANGESET_DIR); + + List renderable = moduleEntries.values().stream() + .filter(e -> !e.changesets().isEmpty() + || (bomContext != null && e.artifactId().equals(bomContext.bomArtifactId()))) + .toList(); + if (renderable.isEmpty()) { + LOG.info("No changesets to write to changelog in {}", this.baseDir); + return changesetsDir; + } + + String changelog = bomContext == null + ? generateMultiModuleChangelog(moduleEntries) + : generateBomChangelog(moduleEntries, bomContext); + + Path changelogFile; + try { + changelogFile = writeChangelog(changelog); + } catch (ChangelogException exception) { + LOG.error("Failed to update changelog at {}", changesetsDir, exception); + return changesetsDir; + } + + deleteConsumedChangesets(moduleEntries.values().stream() + .filter(e -> !e.changesets().isEmpty()) + .toList()); + return changelogFile; + } + + public record ReleaseEntry(String artifactId, String newVersion, List changesets) {} + + /** + * Context for BOM-aware changelog rendering. + * + * @param headerArtifactId The artifactId shown in the top-level {@code ##} release header + * (consumer-parent if configured, otherwise the BOM itself). + * @param headerVersion The version shown in the top-level header (the BOM's version). + * @param bomArtifactId The BOM's artifactId — its section gets the pinned-versions block. + * @param pinnedUpdates Ordered map of pinned module artifactId → new version, as + * applied to the BOM's {@code }. + */ + public record BomContext( + String headerArtifactId, + String headerVersion, + String bomArtifactId, + Map pinnedUpdates + ) {} + + private void deleteConsumedChangesets(List entries) { + entries.stream() + .flatMap(e -> e.changesets().stream()) + .map(Changeset::file) + .filter(java.util.Objects::nonNull) + .distinct() + .forEach(file -> { + try { + Files.deleteIfExists(file.toPath()); + } catch (IOException e) { + LOG.error("Failed to delete {}", file, e); + } + }); + } + + private String generateMultiModuleChangelog(Map moduleEntries) { + String body = moduleEntries.values().stream() + .filter(e -> !e.changesets().isEmpty()) + .map(this::generateModuleSection) + .collect(Collectors.joining("\n\n")); + + String markdown = """ + # Changelog + + %s + """.formatted(body); + + return MarkdownFormatter.format(markdown); + } + + private String generateModuleSection(ReleaseEntry entry) { + String changes = renderChangesByLevel(entry.changesets(), "###"); + return """ + ## %s@%s + + %s""".formatted(entry.artifactId(), entry.newVersion(), changes); + } + + private String generateBomChangelog(Map moduleEntries, BomContext bom) { + List sections = new ArrayList<>(); + for (ReleaseEntry entry : moduleEntries.values()) { + if (entry.artifactId().equals(bom.bomArtifactId())) { + continue; + } + if (entry.changesets().isEmpty()) { + continue; + } + sections.add(generateBomNestedSection(entry)); + } + + ReleaseEntry bomEntry = moduleEntries.get(bom.bomArtifactId()); + if (bomEntry != null) { + sections.add(generateBomOwnSection(bomEntry, bom)); + } + + String body = String.join("\n\n", sections); + + String markdown = """ + # Changelog + + ## %s@%s + + %s + """.formatted(bom.headerArtifactId(), bom.headerVersion(), body); + + return MarkdownFormatter.format(markdown); + } + + private String generateBomNestedSection(ReleaseEntry entry) { + String changes = renderChangesByLevel(entry.changesets(), "####"); + return """ + ### %s@%s + + %s""".formatted(entry.artifactId(), entry.newVersion(), changes); + } + + private String generateBomOwnSection(ReleaseEntry bomEntry, BomContext bom) { + StringBuilder section = new StringBuilder(); + section.append("### ").append(bomEntry.artifactId()).append('@').append(bomEntry.newVersion()).append("\n\n"); + + if (!bomEntry.changesets().isEmpty()) { + section.append(renderChangesByLevel(bomEntry.changesets(), "####")).append("\n\n"); + } + + if (!bom.pinnedUpdates().isEmpty()) { + section.append("#### Pinned version updates\n\n"); + for (Map.Entry e : bom.pinnedUpdates().entrySet()) { + section.append("- ").append(e.getKey()).append('@').append(e.getValue()).append('\n'); + } + } + return section.toString(); + } + private String generateChangelog(String packageName, String version, List changesets) { - String changes = changesets + String changes = renderChangesByLevel(changesets, "###"); + + String markdown = """ + # %s + + ## %s + + %s + """.formatted(packageName, version, changes); + + return MarkdownFormatter.format(markdown); + } + + private String renderChangesByLevel(List changesets, String headingPrefix) { + return changesets .stream() .collect(groupingBy(Changeset::level, mapping(Changeset::message, toList()))) .entrySet() @@ -91,21 +266,11 @@ private String generateChangelog(String packageName, String version, List changes) { diff --git a/changesets-java/src/main/java/se/fortnox/changesets/ChangesetLocator.java b/changesets-java/src/main/java/se/fortnox/changesets/ChangesetLocator.java index 2cde952..27a4342 100644 --- a/changesets-java/src/main/java/se/fortnox/changesets/ChangesetLocator.java +++ b/changesets-java/src/main/java/se/fortnox/changesets/ChangesetLocator.java @@ -21,34 +21,35 @@ public ChangesetLocator(Path baseDir) { } public List getChangesets(String packageName) { - Path changesetsDir = this.baseDir.resolve(CHANGESET_DIR); - File[] array = changesetsDir.toFile().listFiles((dir, name) -> name.endsWith(".md")); - - if (array == null) { - LOG.debug("No changesets found in {}", changesetsDir); - return new ArrayList<>(); - } - - List changesets = Arrays.stream(Objects.requireNonNull(array)) - .sorted() - .toList(); - - List matchingChangesets = changesets.stream() - .flatMap(file -> ChangesetParser.parseFile(file).stream()) + List matchingChangesets = getAllChangesets().stream() .filter(changeset -> { boolean matchesPackage = changeset.packageName().equals(packageName); if (!matchesPackage) { LOG.info("Found {}, but {} did not match requested packagename {}", changeset.file(), changeset.packageName(), packageName); } - return matchesPackage; }) .toList(); if (matchingChangesets.isEmpty()) { - LOG.info("No changesets matching package {} found in {}", packageName, changesetsDir); + LOG.info("No changesets matching package {} found in {}", packageName, this.baseDir.resolve(CHANGESET_DIR)); } return matchingChangesets; } + + public List getAllChangesets() { + Path changesetsDir = this.baseDir.resolve(CHANGESET_DIR); + File[] array = changesetsDir.toFile().listFiles((dir, name) -> name.endsWith(".md")); + + if (array == null) { + LOG.debug("No changesets found in {}", changesetsDir); + return new ArrayList<>(); + } + + return Arrays.stream(Objects.requireNonNull(array)) + .sorted() + .flatMap(file -> ChangesetParser.parseFile(file).stream()) + .toList(); + } } diff --git a/changesets-java/src/main/java/se/fortnox/changesets/ChangesetsConfig.java b/changesets-java/src/main/java/se/fortnox/changesets/ChangesetsConfig.java new file mode 100644 index 0000000..1f300b5 --- /dev/null +++ b/changesets-java/src/main/java/se/fortnox/changesets/ChangesetsConfig.java @@ -0,0 +1,122 @@ +package se.fortnox.changesets; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.slf4j.LoggerFactory.getLogger; + +public record ChangesetsConfig( + VersioningStrategy versioning, + List> linked, + List> fixed, + ChangelogMode changelog, + Bom bom +) { + private static final Logger LOG = getLogger(ChangesetsConfig.class); + public static final String CONFIG_FILE = "config.json"; + + public ChangesetsConfig { + versioning = versioning == null ? VersioningStrategy.FIXED : versioning; + linked = linked == null ? List.of() : List.copyOf(linked); + fixed = fixed == null ? List.of() : List.copyOf(fixed); + changelog = changelog == null ? ChangelogMode.ROOT : changelog; + validateGroupsAreDisjoint(linked, fixed); + } + + public static ChangesetsConfig defaults() { + return new ChangesetsConfig(VersioningStrategy.FIXED, List.of(), List.of(), ChangelogMode.ROOT, null); + } + + /** + * Read .changeset/config.json from the given changesets directory. + * Returns defaults if the file does not exist or cannot be parsed. + */ + public static ChangesetsConfig load(Path changesetsDir) { + Path configFile = changesetsDir.resolve(CONFIG_FILE); + if (!Files.exists(configFile)) { + return defaults(); + } + try { + String json = Files.readString(configFile); + return new ObjectMapper().readValue(json, ChangesetsConfig.class); + } catch (IOException e) { + LOG.error("Failed to read changesets config at {}, falling back to defaults", configFile, e); + return defaults(); + } + } + + private static void validateGroupsAreDisjoint(List> linked, List> fixed) { + Set seen = new HashSet<>(); + List> allGroups = new ArrayList<>(linked.size() + fixed.size()); + allGroups.addAll(linked); + allGroups.addAll(fixed); + for (List group : allGroups) { + for (String name : group) { + if (!seen.add(name)) { + throw new IllegalArgumentException( + "Module '" + name + "' appears in multiple linked/fixed groups"); + } + } + } + } + + public enum VersioningStrategy { + @JsonProperty("fixed") FIXED, + @JsonProperty("independent") INDEPENDENT; + + @JsonCreator + public static VersioningStrategy fromString(String value) { + if (value == null) { + return FIXED; + } + return switch (value.toLowerCase()) { + case "fixed" -> FIXED; + case "independent" -> INDEPENDENT; + default -> throw new IllegalArgumentException("Unknown versioning strategy: " + value); + }; + } + } + + public enum ChangelogMode { + @JsonProperty("root") ROOT; + + @JsonCreator + public static ChangelogMode fromString(String value) { + if (value == null) { + return ROOT; + } + return switch (value.toLowerCase()) { + case "root" -> ROOT; + default -> throw new IllegalArgumentException("Unknown changelog mode: " + value); + }; + } + } + + /** + * Optional BOM (Bill of Materials) configuration. When set, the BOM module's + * {@code } that pin sibling module versions are rewritten on every + * prepare, and the BOM itself is auto-bumped at the max level of any tracked + * module's bump. An optional {@code consumerParent} provides the changelog header + * artifactId; it inherits its version from the BOM via Maven parent inheritance. + */ + public record Bom(String module, String consumerParent) { + public Bom { + if (module == null || module.isBlank()) { + throw new IllegalArgumentException("bom.module must be set"); + } + if (consumerParent != null && consumerParent.isBlank()) { + consumerParent = null; + } + } + } +} diff --git a/changesets-java/src/test/java/se/fortnox/changesets/BumpPlannerTest.java b/changesets-java/src/test/java/se/fortnox/changesets/BumpPlannerTest.java new file mode 100644 index 0000000..7b5e387 --- /dev/null +++ b/changesets-java/src/test/java/se/fortnox/changesets/BumpPlannerTest.java @@ -0,0 +1,309 @@ +package se.fortnox.changesets; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static se.fortnox.changesets.ChangesetsConfig.ChangelogMode.ROOT; +import static se.fortnox.changesets.ChangesetsConfig.VersioningStrategy.FIXED; +import static se.fortnox.changesets.ChangesetsConfig.VersioningStrategy.INDEPENDENT; + +class BumpPlannerTest { + + private static Changeset changeset(String packageName, Level level) { + return new Changeset(packageName, level, "msg", new File("dummy.md")); + } + + @Nested + class FixedStrategy { + @Test + void allModulesBumpTogetherOnAnyChangeset() { + var reactor = Map.of("root", "1.0.0", "m1", "1.0.0", "m2", "1.0.0"); + var changes = List.of(changeset("m1", Level.MINOR)); + + var result = BumpPlanner.plan(changes, reactor, ChangesetsConfig.defaults()); + + assertThat(result).hasSize(3); + assertThat(result.get("root").newVersion()).isEqualTo("1.1.0"); + assertThat(result.get("m1").newVersion()).isEqualTo("1.1.0"); + assertThat(result.get("m2").newVersion()).isEqualTo("1.1.0"); + } + + @Test + void noChangesetsProducesEmptyResult() { + var reactor = Map.of("root", "1.0.0", "m1", "1.0.0"); + + var result = BumpPlanner.plan(List.of(), reactor, ChangesetsConfig.defaults()); + + assertThat(result).isEmpty(); + } + + @Test + void highestBumpLevelWinsAcrossAllChangesets() { + var reactor = Map.of("root", "1.0.0", "m1", "1.0.0"); + var changes = List.of( + changeset("m1", Level.PATCH), + changeset("root", Level.MAJOR)); + + var result = BumpPlanner.plan(changes, reactor, ChangesetsConfig.defaults()); + + assertThat(result.get("m1").newVersion()).isEqualTo("2.0.0"); + assertThat(result.get("root").newVersion()).isEqualTo("2.0.0"); + } + + @Test + void baseVersionIsMaxOfReactorWhenVersionsDiffer() { + var reactor = Map.of("root", "1.0.0", "m1", "2.0.0", "m2", "1.5.0"); + var changes = List.of(changeset("m1", Level.PATCH)); + + var result = BumpPlanner.plan(changes, reactor, ChangesetsConfig.defaults()); + + assertThat(result.values()).allMatch(b -> b.newVersion().equals("2.0.1")); + } + + @Test + void releaseVersionOutranksSnapshotAtSameNumericVersion() { + var reactor = Map.of("root", "1.0.0-SNAPSHOT", "m1", "1.0.0", "m2", "1.0.0-SNAPSHOT"); + var changes = List.of(changeset("m1", Level.PATCH)); + + var result = BumpPlanner.plan(changes, reactor, ChangesetsConfig.defaults()); + + assertThat(result.values()).allMatch(b -> b.newVersion().equals("1.0.1")); + } + } + + @Nested + class IndependentStrategy { + @Test + void modulesWithoutGroupsBumpIndependently() { + var reactor = Map.of("m1", "1.0.0", "m2", "2.0.0"); + var changes = List.of( + changeset("m1", Level.MINOR), + changeset("m2", Level.PATCH)); + var config = new ChangesetsConfig(INDEPENDENT, List.of(), List.of(), ROOT, null); + + var result = BumpPlanner.plan(changes, reactor, config); + + assertThat(result.get("m1").newVersion()).isEqualTo("1.1.0"); + assertThat(result.get("m2").newVersion()).isEqualTo("2.0.1"); + } + + @Test + void modulesWithoutChangesetsAreOmitted() { + var reactor = Map.of("m1", "1.0.0", "m2", "1.0.0"); + var changes = List.of(changeset("m1", Level.PATCH)); + var config = new ChangesetsConfig(INDEPENDENT, List.of(), List.of(), ROOT, null); + + var result = BumpPlanner.plan(changes, reactor, config); + + assertThat(result).containsOnlyKeys("m1"); + assertThat(result.get("m1").newVersion()).isEqualTo("1.0.1"); + } + } + + @Nested + class LinkedGroups { + @Test + void npmDocsExample_pkgA_patch_pkgB_minor_bothAt1_0_0() { + // From changesets.dev/guide/linked-packages Release 1 + var reactor = Map.of("pkg-a", "1.0.0", "pkg-b", "1.0.0", "pkg-c", "1.0.0"); + var changes = List.of( + changeset("pkg-a", Level.PATCH), + changeset("pkg-b", Level.MINOR), + changeset("pkg-c", Level.MAJOR)); + var config = new ChangesetsConfig( + INDEPENDENT, + List.of(List.of("pkg-a", "pkg-b")), + List.of(), + ROOT, + null); + + var result = BumpPlanner.plan(changes, reactor, config); + + assertThat(result.get("pkg-a").newVersion()).isEqualTo("1.1.0"); + assertThat(result.get("pkg-b").newVersion()).isEqualTo("1.1.0"); + assertThat(result.get("pkg-c").newVersion()).isEqualTo("2.0.0"); + } + + @Test + void onlyActiveMembersBumpInLinkedGroup() { + var reactor = Map.of("pkg-a", "1.0.0", "pkg-b", "1.0.0"); + var changes = List.of(changeset("pkg-a", Level.MINOR)); + var config = new ChangesetsConfig( + INDEPENDENT, + List.of(List.of("pkg-a", "pkg-b")), + List.of(), + ROOT, + null); + + var result = BumpPlanner.plan(changes, reactor, config); + + assertThat(result).containsOnlyKeys("pkg-a"); + assertThat(result.get("pkg-a").newVersion()).isEqualTo("1.1.0"); + } + + @Test + void baseVersionIsMaxAcrossAllLinkedMembersIncludingInactive() { + var reactor = Map.of("pkg-a", "1.0.0", "pkg-b", "2.5.0"); + var changes = List.of(changeset("pkg-a", Level.PATCH)); + var config = new ChangesetsConfig( + INDEPENDENT, + List.of(List.of("pkg-a", "pkg-b")), + List.of(), + ROOT, + null); + + var result = BumpPlanner.plan(changes, reactor, config); + + assertThat(result).containsOnlyKeys("pkg-a"); + assertThat(result.get("pkg-a").newVersion()).isEqualTo("2.5.1"); + } + } + + @Nested + class FixedGroups { + @Test + void allFixedMembersBumpWhenAnyHasChangeset() { + var reactor = Map.of("pkg-a", "1.0.0", "pkg-b", "1.0.0", "pkg-c", "1.0.0"); + var changes = List.of(changeset("pkg-a", Level.MINOR)); + var config = new ChangesetsConfig( + INDEPENDENT, + List.of(), + List.of(List.of("pkg-a", "pkg-b")), + ROOT, + null); + + var result = BumpPlanner.plan(changes, reactor, config); + + assertThat(result.get("pkg-a").newVersion()).isEqualTo("1.1.0"); + assertThat(result.get("pkg-b").newVersion()).isEqualTo("1.1.0"); + assertThat(result).doesNotContainKey("pkg-c"); + } + } + + @Nested + class UnknownModules { + @Test + void changesetForUnknownModuleIsIgnored() { + var reactor = Map.of("m1", "1.0.0"); + var changes = List.of(changeset("unknown", Level.PATCH)); + + var result = BumpPlanner.plan(changes, reactor, ChangesetsConfig.defaults()); + + assertThat(result).isEmpty(); + } + } + + @Nested + class BomBumping { + @Test + void bomBumpsAtMaxLevelOfTrackedModules() { + var reactor = Map.of("bom", "1.0.0", "starter-a", "1.0.0", "starter-b", "1.0.0"); + var changes = List.of( + changeset("starter-a", Level.PATCH), + changeset("starter-b", Level.MINOR)); + var config = new ChangesetsConfig( + INDEPENDENT, + List.of(), + List.of(), + ROOT, + new ChangesetsConfig.Bom("bom", null)); + + var result = BumpPlanner.plan(changes, reactor, config); + + assertThat(result.get("bom").newVersion()).isEqualTo("1.1.0"); + assertThat(result.get("bom").changesets()).isEmpty(); + assertThat(result.get("starter-a").newVersion()).isEqualTo("1.0.1"); + assertThat(result.get("starter-b").newVersion()).isEqualTo("1.1.0"); + } + + @Test + void bomKeepsExplicitChangesets() { + var reactor = Map.of("bom", "1.0.0", "starter-a", "1.0.0"); + var changes = List.of( + changeset("starter-a", Level.PATCH), + changeset("bom", Level.MAJOR)); + var config = new ChangesetsConfig( + INDEPENDENT, + List.of(), + List.of(), + ROOT, + new ChangesetsConfig.Bom("bom", null)); + + var result = BumpPlanner.plan(changes, reactor, config); + + assertThat(result.get("bom").newVersion()).isEqualTo("2.0.0"); + assertThat(result.get("bom").changesets()).hasSize(1); + assertThat(result.get("bom").changesets().get(0).level()).isEqualTo(Level.MAJOR); + } + + @Test + void consumerParentIsRemovedFromPlan() { + var reactor = Map.of("bom", "1.0.0", "consumer-parent", "1.0.0", "starter-a", "1.0.0"); + var changes = List.of(changeset("starter-a", Level.PATCH)); + var config = new ChangesetsConfig( + INDEPENDENT, + List.of(), + List.of(), + ROOT, + new ChangesetsConfig.Bom("bom", "consumer-parent")); + + var result = BumpPlanner.plan(changes, reactor, config); + + assertThat(result).doesNotContainKey("consumer-parent"); + assertThat(result).containsKeys("bom", "starter-a"); + } + + @Test + void bomDoesNothingWhenNoTrackedModulesBumpAndNoExplicitChangesets() { + var reactor = Map.of("bom", "1.0.0", "starter-a", "1.0.0"); + var changes = List.of(); + var config = new ChangesetsConfig( + INDEPENDENT, + List.of(), + List.of(), + ROOT, + new ChangesetsConfig.Bom("bom", null)); + + var result = BumpPlanner.plan(changes, reactor, config); + + assertThat(result).isEmpty(); + } + + @Test + void dependencyOnlyChangesetDoesNotBumpBom() { + var reactor = Map.of("bom", "1.0.0", "starter-a", "1.0.0"); + var changes = List.of(changeset("starter-a", Level.DEPENDENCY)); + var config = new ChangesetsConfig( + INDEPENDENT, + List.of(), + List.of(), + ROOT, + new ChangesetsConfig.Bom("bom", null)); + + var result = BumpPlanner.plan(changes, reactor, config); + + assertThat(result).containsOnlyKeys("starter-a"); + } + } + + @Nested + class DependencyOnly { + @Test + void dependencyOnlyChangesetEmitsBumpWithUnchangedVersion() { + var reactor = Map.of("m1", "1.0.0"); + var changes = List.of(changeset("m1", Level.DEPENDENCY)); + var config = new ChangesetsConfig(INDEPENDENT, List.of(), List.of(), ROOT, null); + + var result = BumpPlanner.plan(changes, reactor, config); + + assertThat(result).containsOnlyKeys("m1"); + assertThat(result.get("m1").newVersion()).isEqualTo("1.0.0"); + assertThat(result.get("m1").isVersionChange()).isFalse(); + } + } +} diff --git a/changesets-java/src/test/java/se/fortnox/changesets/ChangelogAggregatorTest.java b/changesets-java/src/test/java/se/fortnox/changesets/ChangelogAggregatorTest.java index 6f6c6d9..e526c29 100644 --- a/changesets-java/src/test/java/se/fortnox/changesets/ChangelogAggregatorTest.java +++ b/changesets-java/src/test/java/se/fortnox/changesets/ChangelogAggregatorTest.java @@ -4,11 +4,15 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import java.io.File; import java.io.IOException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static se.fortnox.changesets.ChangelogAggregator.CHANGELOG_FILE; @@ -297,7 +301,193 @@ void shouldAggregateDependencyUpdates(@TempDir Path tempDir) throws FileAlreadyE that should be kept as a single item - Some dependency - Third dependency - + + """); + } + + @Test + void mergeReleaseToChangelog_writesMultiModuleBlock(@TempDir Path tempDir) throws Exception { + ChangesetWriter writer = new ChangesetWriter(tempDir); + Path patchFile = writer.writeChangeset("pkg-a", Level.PATCH, "Patched A"); + Path minorFile = writer.writeChangeset("pkg-b", Level.MINOR, "Added B feature"); + + Changeset patchChangeset = new Changeset("pkg-a", Level.PATCH, "Patched A", patchFile.toFile()); + Changeset minorChangeset = new Changeset("pkg-b", Level.MINOR, "Added B feature", minorFile.toFile()); + + Map entries = new LinkedHashMap<>(); + entries.put("pkg-a", new ChangelogAggregator.ReleaseEntry("pkg-a", "1.2.3", List.of(patchChangeset))); + entries.put("pkg-b", new ChangelogAggregator.ReleaseEntry("pkg-b", "2.0.0", List.of(minorChangeset))); + + new ChangelogAggregator(tempDir).mergeReleaseToChangelog(entries); + + assertThat(tempDir.resolve(CHANGELOG_FILE)) + .exists() + .content() + .isEqualTo(""" + # Changelog + + ## pkg-a@1.2.3 + + ### Patch Changes + + - Patched A + + ## pkg-b@2.0.0 + + ### Minor Changes + + - Added B feature + + """); + + assertThat(patchFile).doesNotExist(); + assertThat(minorFile).doesNotExist(); + } + + @Test + void mergeReleaseToChangelog_prependsToExistingChangelog(@TempDir Path tempDir) throws Exception { + Files.writeString(tempDir.resolve(CHANGELOG_FILE), """ + # Changelog + + ## pkg-a@1.0.0 + + ### Patch Changes + + - Old change + """, StandardOpenOption.CREATE_NEW); + + ChangesetWriter writer = new ChangesetWriter(tempDir); + Path file = writer.writeChangeset("pkg-a", Level.MINOR, "Newer change"); + Changeset cs = new Changeset("pkg-a", Level.MINOR, "Newer change", file.toFile()); + + Map entries = new LinkedHashMap<>(); + entries.put("pkg-a", new ChangelogAggregator.ReleaseEntry("pkg-a", "1.1.0", List.of(cs))); + + new ChangelogAggregator(tempDir).mergeReleaseToChangelog(entries); + + assertThat(tempDir.resolve(CHANGELOG_FILE)) + .content() + .isEqualTo(""" + # Changelog + + ## pkg-a@1.1.0 + + ### Minor Changes + + - Newer change + + + ## pkg-a@1.0.0 + + ### Patch Changes + + - Old change + """); + } + + @Test + void mergeReleaseToChangelog_bomMode_rendersNestedUnderConsumerParentHeader(@TempDir Path tempDir) throws Exception { + ChangesetWriter writer = new ChangesetWriter(tempDir); + Path aFile = writer.writeChangeset("starter-a", Level.MINOR, "Added feature A"); + Path bFile = writer.writeChangeset("starter-b", Level.PATCH, "Fixed B"); + + Changeset aChange = new Changeset("starter-a", Level.MINOR, "Added feature A", aFile.toFile()); + Changeset bChange = new Changeset("starter-b", Level.PATCH, "Fixed B", bFile.toFile()); + + Map entries = new LinkedHashMap<>(); + entries.put("starter-a", new ChangelogAggregator.ReleaseEntry("starter-a", "0.4.0", List.of(aChange))); + entries.put("starter-b", new ChangelogAggregator.ReleaseEntry("starter-b", "0.3.3", List.of(bChange))); + entries.put("bom-dep", new ChangelogAggregator.ReleaseEntry("bom-dep", "0.3.3", List.of())); + + Map pinned = new LinkedHashMap<>(); + pinned.put("starter-a", "0.4.0"); + pinned.put("starter-b", "0.3.3"); + var bomCtx = new ChangelogAggregator.BomContext("consumer-parent", "0.3.3", "bom-dep", pinned); + + new ChangelogAggregator(tempDir).mergeReleaseToChangelog(entries, bomCtx); + + assertThat(tempDir.resolve(CHANGELOG_FILE)) + .exists() + .content() + .isEqualTo(""" + # Changelog + + ## consumer-parent@0.3.3 + + ### starter-a@0.4.0 + + #### Minor Changes + + - Added feature A + + ### starter-b@0.3.3 + + #### Patch Changes + + - Fixed B + + ### bom-dep@0.3.3 + + #### Pinned version updates + + - starter-a@0.4.0 + - starter-b@0.3.3 + + """); + } + + @Test + void mergeReleaseToChangelog_bomMode_includesExplicitBomChangesetsAboveSyncedVersions(@TempDir Path tempDir) throws Exception { + ChangesetWriter writer = new ChangesetWriter(tempDir); + Path aFile = writer.writeChangeset("starter-a", Level.PATCH, "Fixed A"); + Path bomFile = writer.writeChangeset("bom-dep", Level.MAJOR, "Removed deprecated managed dep"); + + Changeset aChange = new Changeset("starter-a", Level.PATCH, "Fixed A", aFile.toFile()); + Changeset bomChange = new Changeset("bom-dep", Level.MAJOR, "Removed deprecated managed dep", bomFile.toFile()); + + Map entries = new LinkedHashMap<>(); + entries.put("starter-a", new ChangelogAggregator.ReleaseEntry("starter-a", "0.4.1", List.of(aChange))); + entries.put("bom-dep", new ChangelogAggregator.ReleaseEntry("bom-dep", "1.0.0", List.of(bomChange))); + + Map pinned = new LinkedHashMap<>(); + pinned.put("starter-a", "0.4.1"); + var bomCtx = new ChangelogAggregator.BomContext("bom-dep", "1.0.0", "bom-dep", pinned); + + new ChangelogAggregator(tempDir).mergeReleaseToChangelog(entries, bomCtx); + + assertThat(tempDir.resolve(CHANGELOG_FILE)) + .content() + .isEqualTo(""" + # Changelog + + ## bom-dep@1.0.0 + + ### starter-a@0.4.1 + + #### Patch Changes + + - Fixed A + + ### bom-dep@1.0.0 + + #### Major Changes + + - Removed deprecated managed dep + + #### Pinned version updates + + - starter-a@0.4.1 + """); } + + @Test + void mergeReleaseToChangelog_skipsModulesWithNoChangesets(@TempDir Path tempDir) { + Map entries = new LinkedHashMap<>(); + entries.put("pkg-a", new ChangelogAggregator.ReleaseEntry("pkg-a", "1.0.0", List.of())); + + new ChangelogAggregator(tempDir).mergeReleaseToChangelog(entries); + + assertThat(tempDir.resolve(CHANGELOG_FILE)).doesNotExist(); + } } \ No newline at end of file diff --git a/changesets-java/src/test/java/se/fortnox/changesets/ChangesetsConfigTest.java b/changesets-java/src/test/java/se/fortnox/changesets/ChangesetsConfigTest.java new file mode 100644 index 0000000..1c78900 --- /dev/null +++ b/changesets-java/src/test/java/se/fortnox/changesets/ChangesetsConfigTest.java @@ -0,0 +1,159 @@ +package se.fortnox.changesets; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static se.fortnox.changesets.ChangesetsConfig.ChangelogMode.ROOT; +import static se.fortnox.changesets.ChangesetsConfig.VersioningStrategy.FIXED; +import static se.fortnox.changesets.ChangesetsConfig.VersioningStrategy.INDEPENDENT; + +class ChangesetsConfigTest { + + @Nested + class Defaults { + @Test + void defaultsToFixedVersioningAndRootChangelog() { + var config = ChangesetsConfig.defaults(); + + assertThat(config.versioning()).isEqualTo(FIXED); + assertThat(config.linked()).isEmpty(); + assertThat(config.fixed()).isEmpty(); + assertThat(config.changelog()).isEqualTo(ROOT); + } + + @Test + void canonicalConstructorFillsNullsWithDefaults() { + var config = new ChangesetsConfig(null, null, null, null, null); + + assertThat(config.versioning()).isEqualTo(FIXED); + assertThat(config.linked()).isEmpty(); + assertThat(config.fixed()).isEmpty(); + assertThat(config.changelog()).isEqualTo(ROOT); + assertThat(config.bom()).isNull(); + } + } + + @Nested + class Loading { + @TempDir + Path tempDir; + + @Test + void returnsDefaultsWhenConfigFileMissing() { + var config = ChangesetsConfig.load(tempDir); + + assertThat(config).isEqualTo(ChangesetsConfig.defaults()); + } + + @Test + void parsesFullyPopulatedConfig() throws IOException { + Files.writeString(tempDir.resolve("config.json"), """ + { + "versioning": "independent", + "linked": [["pkg-a", "pkg-b"]], + "fixed": [["pkg-c", "pkg-d"]], + "changelog": "root", + "bom": { + "module": "pkg-bom", + "consumerParent": "pkg-parent" + } + } + """); + + var config = ChangesetsConfig.load(tempDir); + + assertThat(config.versioning()).isEqualTo(INDEPENDENT); + assertThat(config.linked()).containsExactly(List.of("pkg-a", "pkg-b")); + assertThat(config.fixed()).containsExactly(List.of("pkg-c", "pkg-d")); + assertThat(config.changelog()).isEqualTo(ROOT); + assertThat(config.bom().module()).isEqualTo("pkg-bom"); + assertThat(config.bom().consumerParent()).isEqualTo("pkg-parent"); + } + + @Test + void parsesBomWithoutConsumerParent() throws IOException { + Files.writeString(tempDir.resolve("config.json"), """ + { + "versioning": "independent", + "bom": { "module": "pkg-bom" } + } + """); + + var config = ChangesetsConfig.load(tempDir); + + assertThat(config.bom().module()).isEqualTo("pkg-bom"); + assertThat(config.bom().consumerParent()).isNull(); + } + + @Test + void appliesDefaultsForMissingFields() throws IOException { + Files.writeString(tempDir.resolve("config.json"), """ + { "versioning": "independent" } + """); + + var config = ChangesetsConfig.load(tempDir); + + assertThat(config.versioning()).isEqualTo(INDEPENDENT); + assertThat(config.linked()).isEmpty(); + assertThat(config.fixed()).isEmpty(); + assertThat(config.changelog()).isEqualTo(ROOT); + } + + @Test + void returnsDefaultsOnMalformedJson() throws IOException { + Files.writeString(tempDir.resolve("config.json"), "not json {"); + + var config = ChangesetsConfig.load(tempDir); + + assertThat(config).isEqualTo(ChangesetsConfig.defaults()); + } + } + + @Nested + class Validation { + @Test + void rejectsModuleInTwoLinkedGroups() { + assertThatThrownBy(() -> new ChangesetsConfig( + INDEPENDENT, + List.of(List.of("pkg-a", "pkg-b"), List.of("pkg-a", "pkg-c")), + List.of(), + ROOT, + null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("pkg-a"); + } + + @Test + void rejectsModuleInBothLinkedAndFixed() { + assertThatThrownBy(() -> new ChangesetsConfig( + INDEPENDENT, + List.of(List.of("pkg-a", "pkg-b")), + List.of(List.of("pkg-a", "pkg-c")), + ROOT, + null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("pkg-a"); + } + + @Test + void allowsDistinctGroups() { + var config = new ChangesetsConfig( + INDEPENDENT, + List.of(List.of("pkg-a", "pkg-b")), + List.of(List.of("pkg-c", "pkg-d")), + ROOT, + null); + + assertThat(config.linked()).hasSize(1); + assertThat(config.fixed()).hasSize(1); + } + } +} diff --git a/changesets-maven-plugin/pom.xml b/changesets-maven-plugin/pom.xml index e78a0b0..f737004 100644 --- a/changesets-maven-plugin/pom.xml +++ b/changesets-maven-plugin/pom.xml @@ -72,6 +72,21 @@ assertj-core test + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.mockito + mockito-core + test + diff --git a/changesets-maven-plugin/src/it/bom-release-plugin-integration/.changeset/bump-a.md b/changesets-maven-plugin/src/it/bom-release-plugin-integration/.changeset/bump-a.md new file mode 100644 index 0000000..ba49566 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-release-plugin-integration/.changeset/bump-a.md @@ -0,0 +1,5 @@ +--- +"starter-a": minor +--- + +Added starter-a feature diff --git a/changesets-maven-plugin/src/it/bom-release-plugin-integration/.changeset/bump-b.md b/changesets-maven-plugin/src/it/bom-release-plugin-integration/.changeset/bump-b.md new file mode 100644 index 0000000..69bace5 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-release-plugin-integration/.changeset/bump-b.md @@ -0,0 +1,5 @@ +--- +"starter-b": patch +--- + +Tiny starter-b fix diff --git a/changesets-maven-plugin/src/it/bom-release-plugin-integration/.changeset/config.json b/changesets-maven-plugin/src/it/bom-release-plugin-integration/.changeset/config.json new file mode 100644 index 0000000..f6b560a --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-release-plugin-integration/.changeset/config.json @@ -0,0 +1,7 @@ +{ + "versioning": "independent", + "bom": { + "module": "bom", + "consumerParent": "consumer-parent" + } +} diff --git a/changesets-maven-plugin/src/it/bom-release-plugin-integration/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/bom-release-plugin-integration/EXPECTED_CHANGELOG.md new file mode 100644 index 0000000..1ccd7bb --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-release-plugin-integration/EXPECTED_CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +## consumer-parent@0.4.0 + +### starter-a@2.1.0 + +#### Minor Changes + +- Added starter-a feature + +### starter-b@3.0.5 + +#### Patch Changes + +- Tiny starter-b fix + +### bom@0.4.0 + +#### Pinned version updates + +- starter-a@2.1.0 +- starter-b@3.0.5 + diff --git a/changesets-maven-plugin/src/it/bom-release-plugin-integration/bom/pom.xml b/changesets-maven-plugin/src/it/bom-release-plugin-integration/bom/pom.xml new file mode 100644 index 0000000..dd19c95 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-release-plugin-integration/bom/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom-rp-root + 1.0.0-SNAPSHOT + + + bom + 0.3.5-SNAPSHOT + pom + + + 2.0.5-SNAPSHOT + 3.0.5-SNAPSHOT + + + + + + se.fortnox.maven.it + starter-a + ${starter-a.version} + + + se.fortnox.maven.it + starter-b + ${starter-b.version} + + + + diff --git a/changesets-maven-plugin/src/it/bom-release-plugin-integration/consumer-parent/pom.xml b/changesets-maven-plugin/src/it/bom-release-plugin-integration/consumer-parent/pom.xml new file mode 100644 index 0000000..7badfab --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-release-plugin-integration/consumer-parent/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom + 0.3.5-SNAPSHOT + ../bom + + + consumer-parent + pom + diff --git a/changesets-maven-plugin/src/it/bom-release-plugin-integration/invoker.properties b/changesets-maven-plugin/src/it/bom-release-plugin-integration/invoker.properties new file mode 100644 index 0000000..099dede --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-release-plugin-integration/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals.1=${project.groupId}:${project.artifactId}:${project.version}:prepare +invoker.goals.2=release:update-versions diff --git a/changesets-maven-plugin/src/it/bom-release-plugin-integration/pom.xml b/changesets-maven-plugin/src/it/bom-release-plugin-integration/pom.xml new file mode 100644 index 0000000..f6fa468 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-release-plugin-integration/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom-rp-root + 1.0.0-SNAPSHOT + pom + + IT: BOM + useReleasePluginIntegration. + Documents the current limitation — release-plugin flow does NOT rewrite BOM <properties> + because that lives in PrepareMojo which returns early when useReleasePluginIntegration=true, + and Maven's VersionPolicy SPI has no hook for cross-module property rewrites. + + + UTF-8 + + + + bom + consumer-parent + starter-a + starter-b + + + + + + maven-release-plugin + 3.1.1 + + changesets + false + + + + se.fortnox.changesets + changesets-maven-plugin + @project.version@ + + + + + + diff --git a/changesets-maven-plugin/src/it/bom-release-plugin-integration/starter-a/pom.xml b/changesets-maven-plugin/src/it/bom-release-plugin-integration/starter-a/pom.xml new file mode 100644 index 0000000..c54df0c --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-release-plugin-integration/starter-a/pom.xml @@ -0,0 +1,13 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom-rp-root + 1.0.0-SNAPSHOT + + + starter-a + 2.0.5-SNAPSHOT + diff --git a/changesets-maven-plugin/src/it/bom-release-plugin-integration/starter-b/pom.xml b/changesets-maven-plugin/src/it/bom-release-plugin-integration/starter-b/pom.xml new file mode 100644 index 0000000..0b37f9c --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-release-plugin-integration/starter-b/pom.xml @@ -0,0 +1,13 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom-rp-root + 1.0.0-SNAPSHOT + + + starter-b + 3.0.5-SNAPSHOT + diff --git a/changesets-maven-plugin/src/it/bom-release-plugin-integration/test.properties b/changesets-maven-plugin/src/it/bom-release-plugin-integration/test.properties new file mode 100644 index 0000000..08a82ac --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-release-plugin-integration/test.properties @@ -0,0 +1 @@ +useReleasePluginIntegration=true diff --git a/changesets-maven-plugin/src/it/bom-release-plugin-integration/verify.groovy b/changesets-maven-plugin/src/it/bom-release-plugin-integration/verify.groovy new file mode 100644 index 0000000..2be382b --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-release-plugin-integration/verify.groovy @@ -0,0 +1,50 @@ +import groovy.xml.XmlSlurper + +import static org.assertj.core.api.Assertions.assertThat + +// Combined flow: `changesets:prepare -DuseReleasePluginIntegration=true` writes VERSIONS +// (no pom edits), then `release:update-versions` consults ChangesetsVersionPolicy to bump +// each module. For the BOM's that pin reactor artifacts through +// , the release-plugin's own rewriter follows the ${prop} +// references and updates the property values — no changesets code needed there. +// +// Modules not present in VERSIONS (root, consumer-parent) are left unchanged by the policy. + +def versions = new File(basedir, '.changeset/VERSIONS').text +assertThat(versions).contains("starter-a=2.1.0") +assertThat(versions).contains("starter-b=3.0.5") +assertThat(versions).contains("bom=0.4.0") +assertThat(versions).doesNotContain("consumer-parent=") +assertThat(versions).doesNotContain("bom-rp-root=") + +// BOM bumped to next-dev SNAPSHOT. +def bom = new XmlSlurper().parse(new File(basedir, 'bom/pom.xml')) +assertThat(bom.version).isEqualTo('0.4.1-SNAPSHOT') + +// BOM that pin reactor artifacts are updated in step by the release-plugin +// rewriter — the sibling starters' next-dev SNAPSHOTs are reflected here. +assertThat(bom.properties.'starter-a.version'.text()).isEqualTo('2.1.1-SNAPSHOT') +assertThat(bom.properties.'starter-b.version'.text()).isEqualTo('3.0.6-SNAPSHOT') + +def starterA = new XmlSlurper().parse(new File(basedir, 'starter-a/pom.xml')) +assertThat(starterA.version).isEqualTo('2.1.1-SNAPSHOT') + +def starterB = new XmlSlurper().parse(new File(basedir, 'starter-b/pom.xml')) +assertThat(starterB.version).isEqualTo('3.0.6-SNAPSHOT') + +// Consumer-parent has no own but its parent ref tracks the BOM. +def consumerParent = new XmlSlurper().parse(new File(basedir, 'consumer-parent/pom.xml')) +assertThat(consumerParent.version.size()).isEqualTo(0) +assertThat(consumerParent.parent.version.text()).isEqualTo('0.4.1-SNAPSHOT') + +// Root not in VERSIONS -> unchanged. +def rootProject = new XmlSlurper().parse(new File(basedir, 'pom.xml')) +assertThat(rootProject.version).isEqualTo('1.0.0-SNAPSHOT') + +assertThat(new File(basedir, 'CHANGELOG.md')) + .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) + +def buildLog = new File(basedir, "build.log").text +assert buildLog =~ /Changesets processed, but not updating POMs due to useReleasePluginIntegration being set to true/ + +true diff --git a/changesets-maven-plugin/src/it/bom-skipBom/.changeset/bump-a.md b/changesets-maven-plugin/src/it/bom-skipBom/.changeset/bump-a.md new file mode 100644 index 0000000..ba49566 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-skipBom/.changeset/bump-a.md @@ -0,0 +1,5 @@ +--- +"starter-a": minor +--- + +Added starter-a feature diff --git a/changesets-maven-plugin/src/it/bom-skipBom/.changeset/bump-b.md b/changesets-maven-plugin/src/it/bom-skipBom/.changeset/bump-b.md new file mode 100644 index 0000000..69bace5 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-skipBom/.changeset/bump-b.md @@ -0,0 +1,5 @@ +--- +"starter-b": patch +--- + +Tiny starter-b fix diff --git a/changesets-maven-plugin/src/it/bom-skipBom/.changeset/config.json b/changesets-maven-plugin/src/it/bom-skipBom/.changeset/config.json new file mode 100644 index 0000000..f6b560a --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-skipBom/.changeset/config.json @@ -0,0 +1,7 @@ +{ + "versioning": "independent", + "bom": { + "module": "bom", + "consumerParent": "consumer-parent" + } +} diff --git a/changesets-maven-plugin/src/it/bom-skipBom/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/bom-skipBom/EXPECTED_CHANGELOG.md new file mode 100644 index 0000000..e37b118 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-skipBom/EXPECTED_CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +## starter-a@2.1.0 + +### Minor Changes + +- Added starter-a feature + +## starter-b@3.0.1 + +### Patch Changes + +- Tiny starter-b fix + diff --git a/changesets-maven-plugin/src/it/bom-skipBom/bom/pom.xml b/changesets-maven-plugin/src/it/bom-skipBom/bom/pom.xml new file mode 100644 index 0000000..df5cba0 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-skipBom/bom/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom-root + 1.0.0 + + + bom + 0.3.0 + pom + + + 2.0.0 + 3.0.0 + + + + + + se.fortnox.maven.it + starter-a + ${starter-a.version} + + + se.fortnox.maven.it + starter-b + ${starter-b.version} + + + + diff --git a/changesets-maven-plugin/src/it/bom-skipBom/consumer-parent/pom.xml b/changesets-maven-plugin/src/it/bom-skipBom/consumer-parent/pom.xml new file mode 100644 index 0000000..c359d9c --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-skipBom/consumer-parent/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom + 0.3.0 + ../bom + + + consumer-parent + pom + diff --git a/changesets-maven-plugin/src/it/prepare-no-version/invoker.properties b/changesets-maven-plugin/src/it/bom-skipBom/invoker.properties similarity index 78% rename from changesets-maven-plugin/src/it/prepare-no-version/invoker.properties rename to changesets-maven-plugin/src/it/bom-skipBom/invoker.properties index 2171af2..dce7e2c 100644 --- a/changesets-maven-plugin/src/it/prepare-no-version/invoker.properties +++ b/changesets-maven-plugin/src/it/bom-skipBom/invoker.properties @@ -1 +1 @@ -invoker.goals=${project.groupId}:${project.artifactId}:${project.version}:prepare \ No newline at end of file +invoker.goals=${project.groupId}:${project.artifactId}:${project.version}:prepare diff --git a/changesets-maven-plugin/src/it/bom-skipBom/pom.xml b/changesets-maven-plugin/src/it/bom-skipBom/pom.xml new file mode 100644 index 0000000..b47fe92 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-skipBom/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom-root + 1.0.0 + pom + + IT: BOM versioning with consumer-parent. + + + UTF-8 + + + bom + consumer-parent + starter-a + starter-b + + diff --git a/changesets-maven-plugin/src/it/bom-skipBom/starter-a/pom.xml b/changesets-maven-plugin/src/it/bom-skipBom/starter-a/pom.xml new file mode 100644 index 0000000..a556a80 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-skipBom/starter-a/pom.xml @@ -0,0 +1,13 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom-root + 1.0.0 + + + starter-a + 2.0.0 + diff --git a/changesets-maven-plugin/src/it/bom-skipBom/starter-b/pom.xml b/changesets-maven-plugin/src/it/bom-skipBom/starter-b/pom.xml new file mode 100644 index 0000000..231eb5a --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-skipBom/starter-b/pom.xml @@ -0,0 +1,13 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom-root + 1.0.0 + + + starter-b + 3.0.0 + diff --git a/changesets-maven-plugin/src/it/bom-skipBom/test.properties b/changesets-maven-plugin/src/it/bom-skipBom/test.properties new file mode 100644 index 0000000..a6c0dba --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-skipBom/test.properties @@ -0,0 +1 @@ +skipBom=true diff --git a/changesets-maven-plugin/src/it/bom-skipBom/verify.groovy b/changesets-maven-plugin/src/it/bom-skipBom/verify.groovy new file mode 100644 index 0000000..cccbf41 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-skipBom/verify.groovy @@ -0,0 +1,33 @@ +import groovy.xml.XmlSlurper + +import static org.assertj.core.api.Assertions.assertThat + +def versions = new File(basedir, '.changeset/VERSIONS').text +assertThat(versions).contains("starter-a=2.1.0") +assertThat(versions).contains("starter-b=3.0.1") +// BOM was NOT bumped under skipBom +assertThat(versions).doesNotContain("bom=") +assertThat(versions).doesNotContain("consumer-parent=") + +// BOM pom completely untouched: version stays, properties stay +def bom = new XmlSlurper().parse(new File(basedir, 'bom/pom.xml')) +assertThat(bom.version).isEqualTo('0.3.0') +assertThat(bom.properties.'starter-a.version'.text()).isEqualTo('2.0.0') +assertThat(bom.properties.'starter-b.version'.text()).isEqualTo('3.0.0') + +// Consumer-parent's parent ref also untouched (BOM didn't bump) +def consumerParent = new XmlSlurper().parse(new File(basedir, 'consumer-parent/pom.xml')) +assertThat(consumerParent.parent.version.text()).isEqualTo('0.3.0') + +// Starters bumped as in plain independent mode +def starterA = new XmlSlurper().parse(new File(basedir, 'starter-a/pom.xml')) +assertThat(starterA.version).isEqualTo('2.1.1-SNAPSHOT') + +def starterB = new XmlSlurper().parse(new File(basedir, 'starter-b/pom.xml')) +assertThat(starterB.version).isEqualTo('3.0.2-SNAPSHOT') + +// Changelog is plain per-module sections (no consumer-parent wrapper, no pinned-versions block) +assertThat(new File(basedir, 'CHANGELOG.md')) + .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) + +true diff --git a/changesets-maven-plugin/src/it/bom-versioning/.changeset/bump-a.md b/changesets-maven-plugin/src/it/bom-versioning/.changeset/bump-a.md new file mode 100644 index 0000000..ba49566 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-versioning/.changeset/bump-a.md @@ -0,0 +1,5 @@ +--- +"starter-a": minor +--- + +Added starter-a feature diff --git a/changesets-maven-plugin/src/it/bom-versioning/.changeset/bump-b.md b/changesets-maven-plugin/src/it/bom-versioning/.changeset/bump-b.md new file mode 100644 index 0000000..69bace5 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-versioning/.changeset/bump-b.md @@ -0,0 +1,5 @@ +--- +"starter-b": patch +--- + +Tiny starter-b fix diff --git a/changesets-maven-plugin/src/it/bom-versioning/.changeset/config.json b/changesets-maven-plugin/src/it/bom-versioning/.changeset/config.json new file mode 100644 index 0000000..f6b560a --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-versioning/.changeset/config.json @@ -0,0 +1,7 @@ +{ + "versioning": "independent", + "bom": { + "module": "bom", + "consumerParent": "consumer-parent" + } +} diff --git a/changesets-maven-plugin/src/it/bom-versioning/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/bom-versioning/EXPECTED_CHANGELOG.md new file mode 100644 index 0000000..069ec2d --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-versioning/EXPECTED_CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +## consumer-parent@0.4.0 + +### starter-a@2.1.0 + +#### Minor Changes + +- Added starter-a feature + +### starter-b@3.0.1 + +#### Patch Changes + +- Tiny starter-b fix + +### bom@0.4.0 + +#### Pinned version updates + +- starter-a@2.1.0 +- starter-b@3.0.1 + diff --git a/changesets-maven-plugin/src/it/bom-versioning/bom/pom.xml b/changesets-maven-plugin/src/it/bom-versioning/bom/pom.xml new file mode 100644 index 0000000..df5cba0 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-versioning/bom/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom-root + 1.0.0 + + + bom + 0.3.0 + pom + + + 2.0.0 + 3.0.0 + + + + + + se.fortnox.maven.it + starter-a + ${starter-a.version} + + + se.fortnox.maven.it + starter-b + ${starter-b.version} + + + + diff --git a/changesets-maven-plugin/src/it/bom-versioning/consumer-parent/pom.xml b/changesets-maven-plugin/src/it/bom-versioning/consumer-parent/pom.xml new file mode 100644 index 0000000..c359d9c --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-versioning/consumer-parent/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom + 0.3.0 + ../bom + + + consumer-parent + pom + diff --git a/changesets-maven-plugin/src/it/bom-versioning/invoker.properties b/changesets-maven-plugin/src/it/bom-versioning/invoker.properties new file mode 100644 index 0000000..dce7e2c --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-versioning/invoker.properties @@ -0,0 +1 @@ +invoker.goals=${project.groupId}:${project.artifactId}:${project.version}:prepare diff --git a/changesets-maven-plugin/src/it/bom-versioning/pom.xml b/changesets-maven-plugin/src/it/bom-versioning/pom.xml new file mode 100644 index 0000000..b47fe92 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-versioning/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom-root + 1.0.0 + pom + + IT: BOM versioning with consumer-parent. + + + UTF-8 + + + bom + consumer-parent + starter-a + starter-b + + diff --git a/changesets-maven-plugin/src/it/bom-versioning/starter-a/pom.xml b/changesets-maven-plugin/src/it/bom-versioning/starter-a/pom.xml new file mode 100644 index 0000000..a556a80 --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-versioning/starter-a/pom.xml @@ -0,0 +1,13 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom-root + 1.0.0 + + + starter-a + 2.0.0 + diff --git a/changesets-maven-plugin/src/it/bom-versioning/starter-b/pom.xml b/changesets-maven-plugin/src/it/bom-versioning/starter-b/pom.xml new file mode 100644 index 0000000..231eb5a --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-versioning/starter-b/pom.xml @@ -0,0 +1,13 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom-root + 1.0.0 + + + starter-b + 3.0.0 + diff --git a/changesets-maven-plugin/src/it/bom-versioning/verify.groovy b/changesets-maven-plugin/src/it/bom-versioning/verify.groovy new file mode 100644 index 0000000..6f852ba --- /dev/null +++ b/changesets-maven-plugin/src/it/bom-versioning/verify.groovy @@ -0,0 +1,40 @@ +import groovy.xml.XmlSlurper + +import static org.assertj.core.api.Assertions.assertThat + +def versions = new File(basedir, '.changeset/VERSIONS').text +assertThat(versions).contains("starter-a=2.1.0") +assertThat(versions).contains("starter-b=3.0.1") +assertThat(versions).contains("bom=0.4.0") +// Consumer parent and root are not in VERSIONS +assertThat(versions).doesNotContain("consumer-parent=") +assertThat(versions).doesNotContain("bom-root=") + +// Root version unchanged +def rootProject = new XmlSlurper().parse(new File(basedir, 'pom.xml')) +assertThat(rootProject.version).isEqualTo('1.0.0') + +// BOM bumped to next-dev SNAPSHOT +def bom = new XmlSlurper().parse(new File(basedir, 'bom/pom.xml')) +assertThat(bom.version).isEqualTo('0.4.1-SNAPSHOT') +// BOM properties rewritten to the starters' next-dev SNAPSHOTs +assertThat(bom.properties.'starter-a.version'.text()).isEqualTo('2.1.1-SNAPSHOT') +assertThat(bom.properties.'starter-b.version'.text()).isEqualTo('3.0.2-SNAPSHOT') + +// Consumer-parent has no own , but its parent ref tracks the BOM +def consumerParent = new XmlSlurper().parse(new File(basedir, 'consumer-parent/pom.xml')) +assertThat(consumerParent.version.size()).isEqualTo(0) +assertThat(consumerParent.parent.artifactId.text()).isEqualTo('bom') +assertThat(consumerParent.parent.version.text()).isEqualTo('0.4.1-SNAPSHOT') + +// Starters bumped to their own next-dev SNAPSHOTs +def starterA = new XmlSlurper().parse(new File(basedir, 'starter-a/pom.xml')) +assertThat(starterA.version).isEqualTo('2.1.1-SNAPSHOT') + +def starterB = new XmlSlurper().parse(new File(basedir, 'starter-b/pom.xml')) +assertThat(starterB.version).isEqualTo('3.0.2-SNAPSHOT') + +assertThat(new File(basedir, 'CHANGELOG.md')) + .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) + +true diff --git a/changesets-maven-plugin/src/it/fixed-groups/.changeset/bump-a.md b/changesets-maven-plugin/src/it/fixed-groups/.changeset/bump-a.md new file mode 100644 index 0000000..5320fb4 --- /dev/null +++ b/changesets-maven-plugin/src/it/fixed-groups/.changeset/bump-a.md @@ -0,0 +1,5 @@ +--- +"module-a": minor +--- + +A change in module-a diff --git a/changesets-maven-plugin/src/it/fixed-groups/.changeset/config.json b/changesets-maven-plugin/src/it/fixed-groups/.changeset/config.json new file mode 100644 index 0000000..983bae7 --- /dev/null +++ b/changesets-maven-plugin/src/it/fixed-groups/.changeset/config.json @@ -0,0 +1,4 @@ +{ + "versioning": "independent", + "fixed": [["module-a", "module-b"]] +} diff --git a/changesets-maven-plugin/src/it/fixed-groups/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/fixed-groups/EXPECTED_CHANGELOG.md new file mode 100644 index 0000000..0029e36 --- /dev/null +++ b/changesets-maven-plugin/src/it/fixed-groups/EXPECTED_CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## module-a@1.3.0 + +### Minor Changes + +- A change in module-a + diff --git a/changesets-maven-plugin/src/it/fixed-groups/invoker.properties b/changesets-maven-plugin/src/it/fixed-groups/invoker.properties new file mode 100644 index 0000000..dce7e2c --- /dev/null +++ b/changesets-maven-plugin/src/it/fixed-groups/invoker.properties @@ -0,0 +1 @@ +invoker.goals=${project.groupId}:${project.artifactId}:${project.version}:prepare diff --git a/changesets-maven-plugin/src/it/fixed-groups/module-a/pom.xml b/changesets-maven-plugin/src/it/fixed-groups/module-a/pom.xml new file mode 100644 index 0000000..ada20fd --- /dev/null +++ b/changesets-maven-plugin/src/it/fixed-groups/module-a/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-a + 1.0.0 + + se.fortnox.maven.it + fixed-root + 1.0.0 + + diff --git a/changesets-maven-plugin/src/it/fixed-groups/module-b/pom.xml b/changesets-maven-plugin/src/it/fixed-groups/module-b/pom.xml new file mode 100644 index 0000000..7b38e5b --- /dev/null +++ b/changesets-maven-plugin/src/it/fixed-groups/module-b/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-b + 1.2.0 + + se.fortnox.maven.it + fixed-root + 1.0.0 + + diff --git a/changesets-maven-plugin/src/it/fixed-groups/module-c/pom.xml b/changesets-maven-plugin/src/it/fixed-groups/module-c/pom.xml new file mode 100644 index 0000000..d31a2d8 --- /dev/null +++ b/changesets-maven-plugin/src/it/fixed-groups/module-c/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-c + 5.0.0 + + se.fortnox.maven.it + fixed-root + 1.0.0 + + diff --git a/changesets-maven-plugin/src/it/fixed-groups/pom.xml b/changesets-maven-plugin/src/it/fixed-groups/pom.xml new file mode 100644 index 0000000..28566c7 --- /dev/null +++ b/changesets-maven-plugin/src/it/fixed-groups/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + se.fortnox.maven.it + fixed-root + 1.0.0 + pom + + IT: fixed sub-group inside independent mode. One changeset bumps all members of the group. + + + UTF-8 + + + module-a + module-b + module-c + + diff --git a/changesets-maven-plugin/src/it/fixed-groups/verify.groovy b/changesets-maven-plugin/src/it/fixed-groups/verify.groovy new file mode 100644 index 0000000..18213d8 --- /dev/null +++ b/changesets-maven-plugin/src/it/fixed-groups/verify.groovy @@ -0,0 +1,26 @@ +import groovy.xml.XmlSlurper + +import static org.assertj.core.api.Assertions.assertThat + +// Fixed group [module-a, module-b]: both bump (even though only module-a had a changeset). +// Base = max(1.0.0, 1.2.0) = 1.2.0; minor bump → 1.3.0 for both. +// module-c is not in the group and has no changeset → no bump. +def versions = new File(basedir, '.changeset/VERSIONS').text +assertThat(versions).contains("module-a=1.3.0") +assertThat(versions).contains("module-b=1.3.0") +assertThat(versions).doesNotContain("module-c=") +assertThat(versions).doesNotContain("fixed-root=") + +def moduleA = new XmlSlurper().parse(new File(basedir, 'module-a/pom.xml')) +assertThat(moduleA.version).isEqualTo('1.3.1-SNAPSHOT') + +def moduleB = new XmlSlurper().parse(new File(basedir, 'module-b/pom.xml')) +assertThat(moduleB.version).isEqualTo('1.3.1-SNAPSHOT') + +def moduleC = new XmlSlurper().parse(new File(basedir, 'module-c/pom.xml')) +assertThat(moduleC.version).isEqualTo('5.0.0') + +assertThat(new File(basedir, 'CHANGELOG.md')) + .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) + +true diff --git a/changesets-maven-plugin/src/it/independent-versioning/.changeset/bump-a.md b/changesets-maven-plugin/src/it/independent-versioning/.changeset/bump-a.md new file mode 100644 index 0000000..4ae38ea --- /dev/null +++ b/changesets-maven-plugin/src/it/independent-versioning/.changeset/bump-a.md @@ -0,0 +1,5 @@ +--- +"module-a": minor +--- + +Added module-a feature diff --git a/changesets-maven-plugin/src/it/independent-versioning/.changeset/bump-b.md b/changesets-maven-plugin/src/it/independent-versioning/.changeset/bump-b.md new file mode 100644 index 0000000..1881197 --- /dev/null +++ b/changesets-maven-plugin/src/it/independent-versioning/.changeset/bump-b.md @@ -0,0 +1,5 @@ +--- +"module-b": patch +--- + +Tiny module-b fix diff --git a/changesets-maven-plugin/src/it/independent-versioning/.changeset/config.json b/changesets-maven-plugin/src/it/independent-versioning/.changeset/config.json new file mode 100644 index 0000000..d19875b --- /dev/null +++ b/changesets-maven-plugin/src/it/independent-versioning/.changeset/config.json @@ -0,0 +1,3 @@ +{ + "versioning": "independent" +} diff --git a/changesets-maven-plugin/src/it/independent-versioning/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/independent-versioning/EXPECTED_CHANGELOG.md new file mode 100644 index 0000000..e598742 --- /dev/null +++ b/changesets-maven-plugin/src/it/independent-versioning/EXPECTED_CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +## module-a@2.1.0 + +### Minor Changes + +- Added module-a feature + +## module-b@3.0.1 + +### Patch Changes + +- Tiny module-b fix + diff --git a/changesets-maven-plugin/src/it/independent-versioning/invoker.properties b/changesets-maven-plugin/src/it/independent-versioning/invoker.properties new file mode 100644 index 0000000..dce7e2c --- /dev/null +++ b/changesets-maven-plugin/src/it/independent-versioning/invoker.properties @@ -0,0 +1 @@ +invoker.goals=${project.groupId}:${project.artifactId}:${project.version}:prepare diff --git a/changesets-maven-plugin/src/it/independent-versioning/module-a/pom.xml b/changesets-maven-plugin/src/it/independent-versioning/module-a/pom.xml new file mode 100644 index 0000000..8a6e5e8 --- /dev/null +++ b/changesets-maven-plugin/src/it/independent-versioning/module-a/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-a + 2.0.0 + + se.fortnox.maven.it + independent-root + 1.0.0 + + diff --git a/changesets-maven-plugin/src/it/independent-versioning/module-b/pom.xml b/changesets-maven-plugin/src/it/independent-versioning/module-b/pom.xml new file mode 100644 index 0000000..065e048 --- /dev/null +++ b/changesets-maven-plugin/src/it/independent-versioning/module-b/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-b + 3.0.0 + + se.fortnox.maven.it + independent-root + 1.0.0 + + diff --git a/changesets-maven-plugin/src/it/independent-versioning/pom.xml b/changesets-maven-plugin/src/it/independent-versioning/pom.xml new file mode 100644 index 0000000..a6b0ca8 --- /dev/null +++ b/changesets-maven-plugin/src/it/independent-versioning/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + se.fortnox.maven.it + independent-root + 1.0.0 + pom + + IT: independent versioning, each submodule bumped from its own current version. + + + UTF-8 + + + module-a + module-b + + diff --git a/changesets-maven-plugin/src/it/independent-versioning/verify.groovy b/changesets-maven-plugin/src/it/independent-versioning/verify.groovy new file mode 100644 index 0000000..9b396fa --- /dev/null +++ b/changesets-maven-plugin/src/it/independent-versioning/verify.groovy @@ -0,0 +1,25 @@ +import groovy.xml.XmlSlurper + +import static org.assertj.core.api.Assertions.assertThat + +def versions = new File(basedir, '.changeset/VERSIONS').text +assertThat(versions).contains("module-a=2.1.0") +assertThat(versions).contains("module-b=3.0.1") +// Root should NOT bump (no changeset targets it) +assertThat(versions).doesNotContain("independent-root=") + +// Root version unchanged +def rootProject = new XmlSlurper().parse(new File(basedir, 'pom.xml')) +assertThat(rootProject.version).isEqualTo('1.0.0') + +// Submodules each bumped to their own next-dev SNAPSHOT +def moduleA = new XmlSlurper().parse(new File(basedir, 'module-a/pom.xml')) +assertThat(moduleA.version).isEqualTo('2.1.1-SNAPSHOT') + +def moduleB = new XmlSlurper().parse(new File(basedir, 'module-b/pom.xml')) +assertThat(moduleB.version).isEqualTo('3.0.2-SNAPSHOT') + +assertThat(new File(basedir, 'CHANGELOG.md')) + .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) + +true diff --git a/changesets-maven-plugin/src/it/linked-groups-multi/.changeset/config.json b/changesets-maven-plugin/src/it/linked-groups-multi/.changeset/config.json new file mode 100644 index 0000000..4038b1f --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups-multi/.changeset/config.json @@ -0,0 +1,4 @@ +{ + "versioning": "independent", + "linked": [["module-a", "module-b"]] +} diff --git a/changesets-maven-plugin/src/it/linked-groups-multi/.changeset/minor-b.md b/changesets-maven-plugin/src/it/linked-groups-multi/.changeset/minor-b.md new file mode 100644 index 0000000..ab89f8a --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups-multi/.changeset/minor-b.md @@ -0,0 +1,5 @@ +--- +"module-b": minor +--- + +Minor change in B diff --git a/changesets-maven-plugin/src/it/linked-groups-multi/.changeset/patch-a.md b/changesets-maven-plugin/src/it/linked-groups-multi/.changeset/patch-a.md new file mode 100644 index 0000000..8709be3 --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups-multi/.changeset/patch-a.md @@ -0,0 +1,5 @@ +--- +"module-a": patch +--- + +Patch in A diff --git a/changesets-maven-plugin/src/it/linked-groups-multi/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/linked-groups-multi/EXPECTED_CHANGELOG.md new file mode 100644 index 0000000..5831015 --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups-multi/EXPECTED_CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +## module-a@1.1.0 + +### Patch Changes + +- Patch in A + +## module-b@1.1.0 + +### Minor Changes + +- Minor change in B + diff --git a/changesets-maven-plugin/src/it/linked-groups-multi/invoker.properties b/changesets-maven-plugin/src/it/linked-groups-multi/invoker.properties new file mode 100644 index 0000000..dce7e2c --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups-multi/invoker.properties @@ -0,0 +1 @@ +invoker.goals=${project.groupId}:${project.artifactId}:${project.version}:prepare diff --git a/changesets-maven-plugin/src/it/linked-groups-multi/module-a/pom.xml b/changesets-maven-plugin/src/it/linked-groups-multi/module-a/pom.xml new file mode 100644 index 0000000..32005fa --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups-multi/module-a/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-a + 1.0.0 + + se.fortnox.maven.it + linked-multi-root + 1.0.0 + + diff --git a/changesets-maven-plugin/src/it/linked-groups-multi/module-b/pom.xml b/changesets-maven-plugin/src/it/linked-groups-multi/module-b/pom.xml new file mode 100644 index 0000000..44313f8 --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups-multi/module-b/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-b + 1.0.0 + + se.fortnox.maven.it + linked-multi-root + 1.0.0 + + diff --git a/changesets-maven-plugin/src/it/linked-groups-multi/pom.xml b/changesets-maven-plugin/src/it/linked-groups-multi/pom.xml new file mode 100644 index 0000000..5d3a4ca --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups-multi/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + se.fortnox.maven.it + linked-multi-root + 1.0.0 + pom + + IT: linked group, both members have changesets — bump together to highest level. + + + UTF-8 + + + module-a + module-b + + diff --git a/changesets-maven-plugin/src/it/linked-groups-multi/verify.groovy b/changesets-maven-plugin/src/it/linked-groups-multi/verify.groovy new file mode 100644 index 0000000..13d40d2 --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups-multi/verify.groovy @@ -0,0 +1,20 @@ +import groovy.xml.XmlSlurper + +import static org.assertj.core.api.Assertions.assertThat + +// Both members have changesets at different levels; both bump to the highest-level result +// of the highest current version in the linked set. From [1.0.0, 1.0.0] + minor = 1.1.0. +def versions = new File(basedir, '.changeset/VERSIONS').text +assertThat(versions).contains("module-a=1.1.0") +assertThat(versions).contains("module-b=1.1.0") + +def moduleA = new XmlSlurper().parse(new File(basedir, 'module-a/pom.xml')) +assertThat(moduleA.version).isEqualTo('1.1.1-SNAPSHOT') + +def moduleB = new XmlSlurper().parse(new File(basedir, 'module-b/pom.xml')) +assertThat(moduleB.version).isEqualTo('1.1.1-SNAPSHOT') + +assertThat(new File(basedir, 'CHANGELOG.md')) + .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) + +true diff --git a/changesets-maven-plugin/src/it/linked-groups/.changeset/bump-a.md b/changesets-maven-plugin/src/it/linked-groups/.changeset/bump-a.md new file mode 100644 index 0000000..fc5b956 --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups/.changeset/bump-a.md @@ -0,0 +1,5 @@ +--- +"module-a": minor +--- + +Added feature in A diff --git a/changesets-maven-plugin/src/it/linked-groups/.changeset/config.json b/changesets-maven-plugin/src/it/linked-groups/.changeset/config.json new file mode 100644 index 0000000..4038b1f --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups/.changeset/config.json @@ -0,0 +1,4 @@ +{ + "versioning": "independent", + "linked": [["module-a", "module-b"]] +} diff --git a/changesets-maven-plugin/src/it/linked-groups/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/linked-groups/EXPECTED_CHANGELOG.md new file mode 100644 index 0000000..9666cbf --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups/EXPECTED_CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## module-a@1.1.0 + +### Minor Changes + +- Added feature in A + diff --git a/changesets-maven-plugin/src/it/linked-groups/invoker.properties b/changesets-maven-plugin/src/it/linked-groups/invoker.properties new file mode 100644 index 0000000..dce7e2c --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups/invoker.properties @@ -0,0 +1 @@ +invoker.goals=${project.groupId}:${project.artifactId}:${project.version}:prepare diff --git a/changesets-maven-plugin/src/it/linked-groups/module-a/pom.xml b/changesets-maven-plugin/src/it/linked-groups/module-a/pom.xml new file mode 100644 index 0000000..636df32 --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups/module-a/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-a + 1.0.0 + + se.fortnox.maven.it + linked-root + 1.0.0 + + diff --git a/changesets-maven-plugin/src/it/linked-groups/module-b/pom.xml b/changesets-maven-plugin/src/it/linked-groups/module-b/pom.xml new file mode 100644 index 0000000..d0129cb --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups/module-b/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-b + 1.0.0 + + se.fortnox.maven.it + linked-root + 1.0.0 + + diff --git a/changesets-maven-plugin/src/it/prepare-no-version/pom.xml b/changesets-maven-plugin/src/it/linked-groups/pom.xml similarity index 63% rename from changesets-maven-plugin/src/it/prepare-no-version/pom.xml rename to changesets-maven-plugin/src/it/linked-groups/pom.xml index 16bcee9..6efe5dc 100644 --- a/changesets-maven-plugin/src/it/prepare-no-version/pom.xml +++ b/changesets-maven-plugin/src/it/linked-groups/pom.xml @@ -4,13 +4,17 @@ 4.0.0 se.fortnox.maven.it - my-package - 1.0.1 + linked-root + 1.0.0 + pom - A simple IT verifying the basic use case. + IT: linked group where only one member has a changeset. UTF-8 - + + module-a + module-b + diff --git a/changesets-maven-plugin/src/it/linked-groups/verify.groovy b/changesets-maven-plugin/src/it/linked-groups/verify.groovy new file mode 100644 index 0000000..7b09920 --- /dev/null +++ b/changesets-maven-plugin/src/it/linked-groups/verify.groovy @@ -0,0 +1,19 @@ +import groovy.xml.XmlSlurper + +import static org.assertj.core.api.Assertions.assertThat + +def versions = new File(basedir, '.changeset/VERSIONS').text +assertThat(versions).contains("module-a=1.1.0") +// Linked: module-b has no changeset, so it MUST NOT bump +assertThat(versions).doesNotContain("module-b=") + +def moduleA = new XmlSlurper().parse(new File(basedir, 'module-a/pom.xml')) +assertThat(moduleA.version).isEqualTo('1.1.1-SNAPSHOT') + +def moduleB = new XmlSlurper().parse(new File(basedir, 'module-b/pom.xml')) +assertThat(moduleB.version).isEqualTo('1.0.0') + +assertThat(new File(basedir, 'CHANGELOG.md')) + .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) + +true diff --git a/changesets-maven-plugin/src/it/multi-module/.changeset/VERSION b/changesets-maven-plugin/src/it/multi-module/.changeset/VERSION deleted file mode 100644 index afaf360..0000000 --- a/changesets-maven-plugin/src/it/multi-module/.changeset/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0.0 \ No newline at end of file diff --git a/changesets-maven-plugin/src/it/multi-module/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/multi-module/EXPECTED_CHANGELOG.md index a904a61..ea44440 100644 --- a/changesets-maven-plugin/src/it/multi-module/EXPECTED_CHANGELOG.md +++ b/changesets-maven-plugin/src/it/multi-module/EXPECTED_CHANGELOG.md @@ -1,6 +1,6 @@ -# multi-module +# Changelog -## 2.0.0 +## multi-module@2.0.0 ### Major Changes diff --git a/changesets-maven-plugin/src/it/multi-module/verify.groovy b/changesets-maven-plugin/src/it/multi-module/verify.groovy index 1d05d17..c73bd77 100644 --- a/changesets-maven-plugin/src/it/multi-module/verify.groovy +++ b/changesets-maven-plugin/src/it/multi-module/verify.groovy @@ -2,28 +2,27 @@ import groovy.xml.XmlSlurper import static org.assertj.core.api.Assertions.assertThat -String expectedVersion = '2.0.0'; -String expectedSnapshot = '2.0.1-SNAPSHOT'; +String expectedVersion = '2.0.0' +String expectedSnapshot = '2.0.1-SNAPSHOT' -// The VERSION file should contain the correct version number -assertThat(new File(basedir, '.changeset/VERSION')) - .content() - .isEqualTo(expectedVersion) +// Default versioning (fixed): all reactor modules appear in VERSIONS at the same version +def versions = new File(basedir, '.changeset/VERSIONS').text +assertThat(versions).contains("multi-module=${expectedVersion}") +assertThat(versions).contains("module1=${expectedVersion}") +assertThat(versions).contains("module2=${expectedVersion}") -// The root pom version should be increased by one patch and be a snapshot +// Root pom version is bumped to next-dev SNAPSHOT def project = new XmlSlurper().parse(new File(basedir, 'pom.xml')) assertThat(project.version).isEqualTo(expectedSnapshot) -// Check that the parent reference is updated to the new version +// Submodule parent refs are synced to the root SNAPSHOT def submodule1 = new XmlSlurper().parse(new File(basedir, 'module1/pom.xml')) assertThat(submodule1.parent.version).isEqualTo(project.version) -// Check that the parent reference is updated to the new version def submodule2 = new XmlSlurper().parse(new File(basedir, 'module2/pom.xml')) assertThat(submodule2.parent.version).isEqualTo(project.version) -// Verify that the CHANGELOG.md has been created correctly assertThat(new File(basedir, 'CHANGELOG.md')) .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) -true \ No newline at end of file +true diff --git a/changesets-maven-plugin/src/it/prepare-no-version/.changeset/nine-owls-knock.md b/changesets-maven-plugin/src/it/prepare-no-version/.changeset/nine-owls-knock.md deleted file mode 100644 index d7e69f0..0000000 --- a/changesets-maven-plugin/src/it/prepare-no-version/.changeset/nine-owls-knock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"my-package": patch ---- - -A tiny change diff --git a/changesets-maven-plugin/src/it/prepare-no-version/.changeset/seven-owls-suspect.md b/changesets-maven-plugin/src/it/prepare-no-version/.changeset/seven-owls-suspect.md deleted file mode 100644 index e37c41c..0000000 --- a/changesets-maven-plugin/src/it/prepare-no-version/.changeset/seven-owls-suspect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"my-package": minor ---- - -A medium change diff --git a/changesets-maven-plugin/src/it/prepare-no-version/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/prepare-no-version/EXPECTED_CHANGELOG.md deleted file mode 100644 index 8c00114..0000000 --- a/changesets-maven-plugin/src/it/prepare-no-version/EXPECTED_CHANGELOG.md +++ /dev/null @@ -1,12 +0,0 @@ -# my-package - -## 0.1.0 - -### Minor Changes - -- A medium change - -### Patch Changes - -- A tiny change - diff --git a/changesets-maven-plugin/src/it/prepare-no-version/verify.groovy b/changesets-maven-plugin/src/it/prepare-no-version/verify.groovy deleted file mode 100644 index 26e5ac4..0000000 --- a/changesets-maven-plugin/src/it/prepare-no-version/verify.groovy +++ /dev/null @@ -1,22 +0,0 @@ -import groovy.xml.XmlSlurper - -import static org.assertj.core.api.Assertions.assertThat - -// Having no VERSIONS file will be treated as version 0.0.0, then incremented one minor by the changesets -String expectedVersion = '0.1.0'; -String expectedSnapshot = '0.1.1-SNAPSHOT'; - -// The VERSION file should contain the correct version number -assertThat(new File(basedir, '.changeset/VERSION')) - .content() - .isEqualTo(expectedVersion) - -// The root pom version should be increased by one patch and be a snapshot -def project = new XmlSlurper().parse(new File(basedir, 'pom.xml')) -assertThat(project.version).isEqualTo(expectedSnapshot) - -// Verify that the CHANGELOG.md has been created correctly -assertThat(new File(basedir, 'CHANGELOG.md')) - .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) - -true \ No newline at end of file diff --git a/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/.changeset/bump-a.md b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/.changeset/bump-a.md new file mode 100644 index 0000000..4ae38ea --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/.changeset/bump-a.md @@ -0,0 +1,5 @@ +--- +"module-a": minor +--- + +Added module-a feature diff --git a/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/.changeset/bump-b.md b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/.changeset/bump-b.md new file mode 100644 index 0000000..1881197 --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/.changeset/bump-b.md @@ -0,0 +1,5 @@ +--- +"module-b": patch +--- + +Tiny module-b fix diff --git a/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/.changeset/config.json b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/.changeset/config.json new file mode 100644 index 0000000..d19875b --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/.changeset/config.json @@ -0,0 +1,3 @@ +{ + "versioning": "independent" +} diff --git a/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/EXPECTED_CHANGELOG.md new file mode 100644 index 0000000..f7cf466 --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/EXPECTED_CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +## module-a@2.1.0 + +### Minor Changes + +- Added module-a feature + +## module-b@3.0.5 + +### Patch Changes + +- Tiny module-b fix + diff --git a/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/invoker.properties b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/invoker.properties new file mode 100644 index 0000000..099dede --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals.1=${project.groupId}:${project.artifactId}:${project.version}:prepare +invoker.goals.2=release:update-versions diff --git a/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/module-a/pom.xml b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/module-a/pom.xml new file mode 100644 index 0000000..a7eb007 --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/module-a/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-a + 2.0.5-SNAPSHOT + + se.fortnox.maven.it + rp-independent-root + 1.0.0-SNAPSHOT + + diff --git a/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/module-b/pom.xml b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/module-b/pom.xml new file mode 100644 index 0000000..de47c9d --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/module-b/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-b + 3.0.5-SNAPSHOT + + se.fortnox.maven.it + rp-independent-root + 1.0.0-SNAPSHOT + + diff --git a/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/pom.xml b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/pom.xml new file mode 100644 index 0000000..cc8a9db --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + se.fortnox.maven.it + rp-independent-root + 1.0.0-SNAPSHOT + pom + + IT: useReleasePluginIntegration + independent versioning across multiple modules. + Verifies that ChangesetsVersionPolicy resolves the release version per module from VERSIONS. + + + UTF-8 + + + + module-a + module-b + + + + + + maven-release-plugin + 3.1.1 + + changesets + false + + + + se.fortnox.changesets + changesets-maven-plugin + @project.version@ + + + + + + diff --git a/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/test.properties b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/test.properties new file mode 100644 index 0000000..08a82ac --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/test.properties @@ -0,0 +1 @@ +useReleasePluginIntegration=true diff --git a/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/verify.groovy b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/verify.groovy new file mode 100644 index 0000000..03a5c52 --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-release-plugin-integration-multimodule/verify.groovy @@ -0,0 +1,31 @@ +import groovy.xml.XmlSlurper + +import static org.assertj.core.api.Assertions.assertThat + +// changesets:prepare wrote VERSIONS but did NOT touch any poms (useReleasePluginIntegration=true). +// module-a 2.0.5-SNAPSHOT + minor -> release 2.1.0 (minor escalates from non-boundary) +// module-b 3.0.5-SNAPSHOT + patch -> release 3.0.5 (patch confirms the intended SNAPSHOT) +def versions = new File(basedir, '.changeset/VERSIONS').text +assertThat(versions).contains("module-a=2.1.0") +assertThat(versions).contains("module-b=3.0.5") +assertThat(versions).doesNotContain("rp-independent-root=") + +// release:update-versions then consulted ChangesetsVersionPolicy per module, which resolved +// the next development version from VERSIONS keyed by artifactId. +def moduleA = new XmlSlurper().parse(new File(basedir, 'module-a/pom.xml')) +assertThat(moduleA.version).isEqualTo('2.1.1-SNAPSHOT') + +def moduleB = new XmlSlurper().parse(new File(basedir, 'module-b/pom.xml')) +assertThat(moduleB.version).isEqualTo('3.0.6-SNAPSHOT') + +// Root has no VERSIONS entry, so its version is unchanged. +def rootProject = new XmlSlurper().parse(new File(basedir, 'pom.xml')) +assertThat(rootProject.version).isEqualTo('1.0.0-SNAPSHOT') + +assertThat(new File(basedir, 'CHANGELOG.md')) + .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) + +def buildLog = new File(basedir, "build.log").text +assert buildLog =~ /Changesets processed, but not updating POMs due to useReleasePluginIntegration being set to true/ + +true diff --git a/changesets-maven-plugin/src/it/prepare-release-plugin-integration/.changeset/VERSION b/changesets-maven-plugin/src/it/prepare-release-plugin-integration/.changeset/VERSION deleted file mode 100644 index 26f8b8b..0000000 --- a/changesets-maven-plugin/src/it/prepare-release-plugin-integration/.changeset/VERSION +++ /dev/null @@ -1 +0,0 @@ -2.4.5 \ No newline at end of file diff --git a/changesets-maven-plugin/src/it/prepare-release-plugin-integration/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/prepare-release-plugin-integration/EXPECTED_CHANGELOG.md index 4104f4f..3f636da 100644 --- a/changesets-maven-plugin/src/it/prepare-release-plugin-integration/EXPECTED_CHANGELOG.md +++ b/changesets-maven-plugin/src/it/prepare-release-plugin-integration/EXPECTED_CHANGELOG.md @@ -1,6 +1,6 @@ -# my-package +# Changelog -## 2.5.0 +## my-package@2.5.0 ### Minor Changes diff --git a/changesets-maven-plugin/src/it/prepare-release-plugin-integration/verify.groovy b/changesets-maven-plugin/src/it/prepare-release-plugin-integration/verify.groovy index f4502bb..ecfca9f 100644 --- a/changesets-maven-plugin/src/it/prepare-release-plugin-integration/verify.groovy +++ b/changesets-maven-plugin/src/it/prepare-release-plugin-integration/verify.groovy @@ -2,22 +2,20 @@ import groovy.xml.XmlSlurper import static org.assertj.core.api.Assertions.assertThat -String expectedVersion = '2.5.0'; -String expectedSnapshot = '2.5.1-SNAPSHOT'; +String expectedVersion = '2.5.0' +String expectedSnapshot = '2.5.1-SNAPSHOT' -// The VERSION file should contain the correct version number -assertThat(new File(basedir, '.changeset/VERSION')) +assertThat(new File(basedir, '.changeset/VERSIONS')) .content() - .isEqualTo(expectedVersion) + .isEqualToIgnoringNewLines("my-package=${expectedVersion}") -// The root pom version should be increased by one patch and be a snapshot +// release:update-versions sets the pom to the next development version derived from VERSIONS def project = new XmlSlurper().parse(new File(basedir, 'pom.xml')) assertThat(project.version).isEqualTo(expectedSnapshot) -// Verify that the CHANGELOG.md has been created correctly assertThat(new File(basedir, 'CHANGELOG.md')) .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) -def buildLog = new File( basedir, "build.log").text +def buildLog = new File(basedir, "build.log").text assert buildLog =~ /Changesets processed, but not updating POMs due to useReleasePluginIntegration being set to true/ -true \ No newline at end of file +true diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/.changeset/bump-a.md b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/.changeset/bump-a.md new file mode 100644 index 0000000..ba49566 --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/.changeset/bump-a.md @@ -0,0 +1,5 @@ +--- +"starter-a": minor +--- + +Added starter-a feature diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/.changeset/bump-b.md b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/.changeset/bump-b.md new file mode 100644 index 0000000..69bace5 --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/.changeset/bump-b.md @@ -0,0 +1,5 @@ +--- +"starter-b": patch +--- + +Tiny starter-b fix diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/.changeset/config.json b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/.changeset/config.json new file mode 100644 index 0000000..f6b560a --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/.changeset/config.json @@ -0,0 +1,7 @@ +{ + "versioning": "independent", + "bom": { + "module": "bom", + "consumerParent": "consumer-parent" + } +} diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/EXPECTED_CHANGELOG.md new file mode 100644 index 0000000..1ccd7bb --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/EXPECTED_CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +## consumer-parent@0.4.0 + +### starter-a@2.1.0 + +#### Minor Changes + +- Added starter-a feature + +### starter-b@3.0.5 + +#### Patch Changes + +- Tiny starter-b fix + +### bom@0.4.0 + +#### Pinned version updates + +- starter-a@2.1.0 +- starter-b@3.0.5 + diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/bom/pom.xml b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/bom/pom.xml new file mode 100644 index 0000000..2f0dbf8 --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/bom/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + se.fortnox.maven.it + ptrp-bom-root + 1.0.0-SNAPSHOT + + + bom + 0.3.5-SNAPSHOT + pom + + + 2.0.5-SNAPSHOT + 3.0.5-SNAPSHOT + + + + + + se.fortnox.maven.it + starter-a + ${starter-a.version} + + + se.fortnox.maven.it + starter-b + ${starter-b.version} + + + + diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/consumer-parent/pom.xml b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/consumer-parent/pom.xml new file mode 100644 index 0000000..7badfab --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/consumer-parent/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + se.fortnox.maven.it + bom + 0.3.5-SNAPSHOT + ../bom + + + consumer-parent + pom + diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/invoker.properties b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/invoker.properties new file mode 100644 index 0000000..099dede --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals.1=${project.groupId}:${project.artifactId}:${project.version}:prepare +invoker.goals.2=release:update-versions diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/pom.xml b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/pom.xml new file mode 100644 index 0000000..b6f0037 --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + se.fortnox.maven.it + ptrp-bom-root + 1.0.0-SNAPSHOT + pom + + IT: option-#4 flow with a BOM. changesets:prepare bumps starters + BOM to unique + next-dev SNAPSHOTs and rewrites BOM <properties>; release-plugin then resolves each + module unambiguously via the policy. + + + UTF-8 + + + + bom + consumer-parent + starter-a + starter-b + + + + + + maven-release-plugin + 3.1.1 + + changesets + false + + + + se.fortnox.changesets + changesets-maven-plugin + @project.version@ + + + + + + diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/starter-a/pom.xml b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/starter-a/pom.xml new file mode 100644 index 0000000..94f0e9e --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/starter-a/pom.xml @@ -0,0 +1,13 @@ + + + 4.0.0 + + se.fortnox.maven.it + ptrp-bom-root + 1.0.0-SNAPSHOT + + + starter-a + 2.0.5-SNAPSHOT + diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/starter-b/pom.xml b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/starter-b/pom.xml new file mode 100644 index 0000000..71f932f --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/starter-b/pom.xml @@ -0,0 +1,13 @@ + + + 4.0.0 + + se.fortnox.maven.it + ptrp-bom-root + 1.0.0-SNAPSHOT + + + starter-b + 3.0.5-SNAPSHOT + diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/verify.groovy b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/verify.groovy new file mode 100644 index 0000000..16e41a0 --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-bom/verify.groovy @@ -0,0 +1,47 @@ +import groovy.xml.XmlSlurper + +import static org.assertj.core.api.Assertions.assertThat + +// Option #4 with a BOM. changesets:prepare rewrites all affected poms + BOM properties +// to unique next-dev SNAPSHOTs; release-plugin's policy resolution is unambiguous. + +def versions = new File(basedir, '.changeset/VERSIONS').text +assertThat(versions).contains("starter-a=2.1.0") +assertThat(versions).contains("starter-b=3.0.5") +assertThat(versions).contains("bom=0.4.0") +assertThat(versions).doesNotContain("consumer-parent=") +assertThat(versions).doesNotContain("ptrp-bom-root=") + +// Post-prepare poms — each at its unique next-dev SNAPSHOT. +def bom = new XmlSlurper().parse(new File(basedir, 'bom/pom.xml')) +assertThat(bom.version).isEqualTo('0.4.1-SNAPSHOT') + +// BOM were rewritten by changesets:prepare (not by release-plugin this time). +assertThat(bom.properties.'starter-a.version'.text()).isEqualTo('2.1.1-SNAPSHOT') +assertThat(bom.properties.'starter-b.version'.text()).isEqualTo('3.0.6-SNAPSHOT') + +def starterA = new XmlSlurper().parse(new File(basedir, 'starter-a/pom.xml')) +assertThat(starterA.version).isEqualTo('2.1.1-SNAPSHOT') + +def starterB = new XmlSlurper().parse(new File(basedir, 'starter-b/pom.xml')) +assertThat(starterB.version).isEqualTo('3.0.6-SNAPSHOT') + +def consumerParent = new XmlSlurper().parse(new File(basedir, 'consumer-parent/pom.xml')) +assertThat(consumerParent.version.size()).isEqualTo(0) +assertThat(consumerParent.parent.version.text()).isEqualTo('0.4.1-SNAPSHOT') + +def rootProject = new XmlSlurper().parse(new File(basedir, 'pom.xml')) +assertThat(rootProject.version).isEqualTo('1.0.0-SNAPSHOT') + +assertThat(new File(basedir, 'CHANGELOG.md')) + .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) + +def buildLog = new File(basedir, "build.log").text +assert buildLog =~ /Resolved release version for bom from VERSIONS: 0\.4\.0/ +assert buildLog =~ /Resolved release version for starter-a from VERSIONS: 2\.1\.0/ +assert buildLog =~ /Resolved release version for starter-b from VERSIONS: 3\.0\.5/ +assert !(buildLog =~ /Ambiguous VERSIONS lookup/) : "Expected no ambiguity in option-#4 flow" +assert !(buildLog =~ /useReleasePluginIntegration being set to true/) : \ + "This IT must NOT use useReleasePluginIntegration" + +true diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/.changeset/bump-a.md b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/.changeset/bump-a.md new file mode 100644 index 0000000..4ae38ea --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/.changeset/bump-a.md @@ -0,0 +1,5 @@ +--- +"module-a": minor +--- + +Added module-a feature diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/.changeset/bump-b.md b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/.changeset/bump-b.md new file mode 100644 index 0000000..1881197 --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/.changeset/bump-b.md @@ -0,0 +1,5 @@ +--- +"module-b": patch +--- + +Tiny module-b fix diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/.changeset/config.json b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/.changeset/config.json new file mode 100644 index 0000000..d19875b --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/.changeset/config.json @@ -0,0 +1,3 @@ +{ + "versioning": "independent" +} diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/EXPECTED_CHANGELOG.md new file mode 100644 index 0000000..f7cf466 --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/EXPECTED_CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +## module-a@2.1.0 + +### Minor Changes + +- Added module-a feature + +## module-b@3.0.5 + +### Patch Changes + +- Tiny module-b fix + diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/invoker.properties b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/invoker.properties new file mode 100644 index 0000000..099dede --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals.1=${project.groupId}:${project.artifactId}:${project.version}:prepare +invoker.goals.2=release:update-versions diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/module-a/pom.xml b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/module-a/pom.xml new file mode 100644 index 0000000..d629704 --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/module-a/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-a + 2.0.5-SNAPSHOT + + se.fortnox.maven.it + ptrp-root + 1.0.0-SNAPSHOT + + diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/module-b/pom.xml b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/module-b/pom.xml new file mode 100644 index 0000000..5f499ed --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/module-b/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-b + 3.0.5-SNAPSHOT + + se.fortnox.maven.it + ptrp-root + 1.0.0-SNAPSHOT + + diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/pom.xml b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/pom.xml new file mode 100644 index 0000000..0d10563 --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + se.fortnox.maven.it + ptrp-root + 1.0.0-SNAPSHOT + pom + + IT: option-#4 flow — `changesets:prepare` bumps poms to unique next-dev SNAPSHOTs, + then release-plugin uses ChangesetsVersionPolicy to resolve each module unambiguously. + No useReleasePluginIntegration flag involved. This is the recommended flow. + + + UTF-8 + + + + module-a + module-b + + + + + + maven-release-plugin + 3.1.1 + + changesets + false + + + + se.fortnox.changesets + changesets-maven-plugin + @project.version@ + + + + + + diff --git a/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/verify.groovy b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/verify.groovy new file mode 100644 index 0000000..9941e1e --- /dev/null +++ b/changesets-maven-plugin/src/it/prepare-then-release-plugin-multimodule/verify.groovy @@ -0,0 +1,56 @@ +import groovy.xml.XmlSlurper + +import static org.assertj.core.api.Assertions.assertThat + +// ----------------------------------------------------------------------------- +// Option #4: the recommended flow when combining changesets with release-plugin. +// +// Step 1 — `changesets:prepare` (no useReleasePluginIntegration): +// Bumps each affected module's pom to its next-dev SNAPSHOT (unique per module). +// Writes .changeset/VERSIONS with the release-target versions. +// Writes CHANGELOG.md and deletes changesets. +// +// Step 2 — `release:update-versions`: +// Consults ChangesetsVersionPolicy per module. Because each module now has a +// unique -SNAPSHOT, `identifyModule` returns a single candidate every time — +// no ambiguity even under independent versioning. +// For dev-version: policy returns nextDevelopmentVersion(release) which equals +// the current SNAPSHOT, so poms don't change again. +// ----------------------------------------------------------------------------- + +// VERSIONS holds the release-target versions from changesets:prepare. +def versions = new File(basedir, '.changeset/VERSIONS').text +assertThat(versions).contains("module-a=2.1.0") +assertThat(versions).contains("module-b=3.0.5") +assertThat(versions).doesNotContain("ptrp-root=") + +// After changesets:prepare, each module is at its own next-dev SNAPSHOT: +// module-a 2.0.5-SNAPSHOT + minor -> release 2.1.0 -> next-dev 2.1.1-SNAPSHOT +// module-b 3.0.5-SNAPSHOT + patch -> release 3.0.5 -> next-dev 3.0.6-SNAPSHOT +def moduleA = new XmlSlurper().parse(new File(basedir, 'module-a/pom.xml')) +assertThat(moduleA.version).isEqualTo('2.1.1-SNAPSHOT') + +def moduleB = new XmlSlurper().parse(new File(basedir, 'module-b/pom.xml')) +assertThat(moduleB.version).isEqualTo('3.0.6-SNAPSHOT') + +// Root wasn't targeted by any changeset -> unchanged. +def rootProject = new XmlSlurper().parse(new File(basedir, 'pom.xml')) +assertThat(rootProject.version).isEqualTo('1.0.0-SNAPSHOT') + +assertThat(new File(basedir, 'CHANGELOG.md')) + .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) + +// The policy identified each module uniquely — no ambiguity warning, no prompt. +def buildLog = new File(basedir, "build.log").text +assert buildLog =~ /Resolved release version for module-a from VERSIONS: 2\.1\.0/ +assert buildLog =~ /Resolved release version for module-b from VERSIONS: 3\.0\.5/ +assert !(buildLog =~ /Ambiguous VERSIONS lookup/) : "Expected no ambiguity in option-#4 flow" +// Root is not in VERSIONS -> policy leaves it alone; that's expected. +assert buildLog =~ /No VERSIONS match for version 1\.0\.0-SNAPSHOT; leaving version unchanged/ + +// Sanity: no "Changesets processed, but not updating POMs" line — that would mean +// useReleasePluginIntegration snuck in, which is the *wrong* flow for this IT. +assert !(buildLog =~ /useReleasePluginIntegration being set to true/) : \ + "This IT must NOT use useReleasePluginIntegration; option #4 relies on changesets:prepare touching the poms" + +true diff --git a/changesets-maven-plugin/src/it/prepare/.changeset/VERSION b/changesets-maven-plugin/src/it/prepare/.changeset/VERSION deleted file mode 100644 index 26f8b8b..0000000 --- a/changesets-maven-plugin/src/it/prepare/.changeset/VERSION +++ /dev/null @@ -1 +0,0 @@ -2.4.5 \ No newline at end of file diff --git a/changesets-maven-plugin/src/it/prepare/EXPECTED_CHANGELOG.md b/changesets-maven-plugin/src/it/prepare/EXPECTED_CHANGELOG.md index 4104f4f..3f636da 100644 --- a/changesets-maven-plugin/src/it/prepare/EXPECTED_CHANGELOG.md +++ b/changesets-maven-plugin/src/it/prepare/EXPECTED_CHANGELOG.md @@ -1,6 +1,6 @@ -# my-package +# Changelog -## 2.5.0 +## my-package@2.5.0 ### Minor Changes diff --git a/changesets-maven-plugin/src/it/prepare/pom.xml b/changesets-maven-plugin/src/it/prepare/pom.xml index 16bcee9..2e65780 100644 --- a/changesets-maven-plugin/src/it/prepare/pom.xml +++ b/changesets-maven-plugin/src/it/prepare/pom.xml @@ -5,7 +5,7 @@ se.fortnox.maven.it my-package - 1.0.1 + 2.4.5 A simple IT verifying the basic use case. diff --git a/changesets-maven-plugin/src/it/prepare/verify.groovy b/changesets-maven-plugin/src/it/prepare/verify.groovy index eaa9b7f..93b9df2 100644 --- a/changesets-maven-plugin/src/it/prepare/verify.groovy +++ b/changesets-maven-plugin/src/it/prepare/verify.groovy @@ -2,20 +2,17 @@ import groovy.xml.XmlSlurper import static org.assertj.core.api.Assertions.assertThat -String expectedVersion = '2.5.0'; -String expectedSnapshot = '2.5.1-SNAPSHOT'; +String expectedVersion = '2.5.0' +String expectedSnapshot = '2.5.1-SNAPSHOT' -// The VERSION file should contain the correct version number -assertThat(new File(basedir, '.changeset/VERSION')) +assertThat(new File(basedir, '.changeset/VERSIONS')) .content() - .isEqualTo(expectedVersion) + .isEqualToIgnoringNewLines("my-package=${expectedVersion}") -// The root pom version should be increased by one patch and be a snapshot def project = new XmlSlurper().parse(new File(basedir, 'pom.xml')) assertThat(project.version).isEqualTo(expectedSnapshot) -// Verify that the CHANGELOG.md has been created correctly assertThat(new File(basedir, 'CHANGELOG.md')) .hasSameTextualContentAs(new File(basedir, 'EXPECTED_CHANGELOG.md')) -true \ No newline at end of file +true diff --git a/changesets-maven-plugin/src/it/release-multimodule/.changeset/VERSION b/changesets-maven-plugin/src/it/release-multimodule/.changeset/VERSION deleted file mode 100644 index fad066f..0000000 --- a/changesets-maven-plugin/src/it/release-multimodule/.changeset/VERSION +++ /dev/null @@ -1 +0,0 @@ -2.5.0 \ No newline at end of file diff --git a/changesets-maven-plugin/src/it/release-multimodule/.changeset/VERSIONS b/changesets-maven-plugin/src/it/release-multimodule/.changeset/VERSIONS new file mode 100644 index 0000000..eda2756 --- /dev/null +++ b/changesets-maven-plugin/src/it/release-multimodule/.changeset/VERSIONS @@ -0,0 +1,3 @@ +multi-module=2.5.0 +module1=2.5.0 +module2=2.5.0 diff --git a/changesets-maven-plugin/src/it/release-multimodule/verify.groovy b/changesets-maven-plugin/src/it/release-multimodule/verify.groovy index 2160626..10937b0 100644 --- a/changesets-maven-plugin/src/it/release-multimodule/verify.groovy +++ b/changesets-maven-plugin/src/it/release-multimodule/verify.groovy @@ -2,24 +2,16 @@ import groovy.xml.XmlSlurper import static org.assertj.core.api.Assertions.assertThat -String expectedVersion = '2.5.0'; +String expectedVersion = '2.5.0' -// The VERSION file should contain the correct version number -assertThat(new File(basedir, '.changeset/VERSION')) - .content() - .isEqualTo(expectedVersion) - -// The root pom version should have the same value as the VERSION file def project = new XmlSlurper().parse(new File(basedir, 'pom.xml')) assertThat(project.version).isEqualTo(expectedVersion) -// Check that the parent reference is updated to the new version +// Submodules inherit version via ; check the parent ref was synced def submodule1 = new XmlSlurper().parse(new File(basedir, 'module1/pom.xml')) -assertThat(submodule1.parent.version).isEqualTo(project.version) - +assertThat(submodule1.parent.version).isEqualTo(expectedVersion) -// Check that the parent reference is updated to the new version def submodule2 = new XmlSlurper().parse(new File(basedir, 'module2/pom.xml')) -assertThat(submodule2.parent.version).isEqualTo(project.version) +assertThat(submodule2.parent.version).isEqualTo(expectedVersion) -true \ No newline at end of file +true diff --git a/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/.changeset/bump-a.md b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/.changeset/bump-a.md new file mode 100644 index 0000000..b0c18f8 --- /dev/null +++ b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/.changeset/bump-a.md @@ -0,0 +1,5 @@ +--- +"module-a": minor +--- + +module-a gets a minor bump -> 1.1.0 diff --git a/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/.changeset/bump-b.md b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/.changeset/bump-b.md new file mode 100644 index 0000000..b2bdfb5 --- /dev/null +++ b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/.changeset/bump-b.md @@ -0,0 +1,5 @@ +--- +"module-b": major +--- + +module-b gets a major bump -> 2.0.0 diff --git a/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/.changeset/config.json b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/.changeset/config.json new file mode 100644 index 0000000..d19875b --- /dev/null +++ b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/.changeset/config.json @@ -0,0 +1,3 @@ +{ + "versioning": "independent" +} diff --git a/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/invoker.properties b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/invoker.properties new file mode 100644 index 0000000..8d1486b --- /dev/null +++ b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals.1=${project.groupId}:${project.artifactId}:${project.version}:prepare +invoker.goals.2=release:update-versions -Dinteractive=false diff --git a/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/module-a/pom.xml b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/module-a/pom.xml new file mode 100644 index 0000000..275f0da --- /dev/null +++ b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/module-a/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-a + 1.0.5-SNAPSHOT + + se.fortnox.maven.it + rpi-ambiguous-root + 1.0.0-SNAPSHOT + + diff --git a/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/module-b/pom.xml b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/module-b/pom.xml new file mode 100644 index 0000000..a87ba55 --- /dev/null +++ b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/module-b/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-b + 1.0.5-SNAPSHOT + + se.fortnox.maven.it + rpi-ambiguous-root + 1.0.0-SNAPSHOT + + diff --git a/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/pom.xml b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/pom.xml new file mode 100644 index 0000000..c38cb6a --- /dev/null +++ b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + se.fortnox.maven.it + rpi-ambiguous-root + 1.0.0-SNAPSHOT + pom + + IT: useReleasePluginIntegration + independent versioning + two modules sharing + the same current version but bumping to DIFFERENT targets. This is the one case where the + policy cannot disambiguate. The IT asserts graceful degradation: no prompt, no crash, a + clear actionable warning in the log, and poms left unchanged so the user can rerun with the + recommended option-#4 flow. + + + UTF-8 + + + + module-a + module-b + + + + + + maven-release-plugin + 3.1.1 + + changesets + false + + + + se.fortnox.changesets + changesets-maven-plugin + @project.version@ + + + + + + diff --git a/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/test.properties b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/test.properties new file mode 100644 index 0000000..08a82ac --- /dev/null +++ b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/test.properties @@ -0,0 +1 @@ +useReleasePluginIntegration=true diff --git a/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/verify.groovy b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/verify.groovy new file mode 100644 index 0000000..0ebb4f4 --- /dev/null +++ b/changesets-maven-plugin/src/it/release-plugin-integration-ambiguous/verify.groovy @@ -0,0 +1,37 @@ +import groovy.xml.XmlSlurper + +import static org.assertj.core.api.Assertions.assertThat + +// Setup: module-a and module-b both at 1.0.5-SNAPSHOT (same current version). +// module-a minor -> 1.1.0, module-b major -> 2.0.0 (differing targets in VERSIONS). +// +// With useReleasePluginIntegration=true, changesets:prepare leaves the poms at +// 1.0.5-SNAPSHOT. When release-plugin then asks the policy for a version, the +// policy sees two candidates at 1.0.5-SNAPSHOT with different targets — genuinely +// ambiguous. The policy leaves the version unchanged and emits an actionable WARN. + +// changesets:prepare wrote both targets to VERSIONS. +def versions = new File(basedir, '.changeset/VERSIONS').text +assertThat(versions).contains("module-a=1.1.0") +assertThat(versions).contains("module-b=2.0.0") + +// Poms remain at their pre-prepare SNAPSHOT (prepare skipped pom edits under the flag, +// and release:update-versions kept them because the policy fell back on ambiguity). +def moduleA = new XmlSlurper().parse(new File(basedir, 'module-a/pom.xml')) +assertThat(moduleA.version).isEqualTo('1.0.5-SNAPSHOT') + +def moduleB = new XmlSlurper().parse(new File(basedir, 'module-b/pom.xml')) +assertThat(moduleB.version).isEqualTo('1.0.5-SNAPSHOT') + +// The build log contains the actionable ambiguity WARN — no silent guess. +def buildLog = new File(basedir, "build.log").text +assert buildLog =~ /Ambiguous VERSIONS lookup for version 1\.0\.5-SNAPSHOT/ +assert buildLog =~ /differing targets/ +assert buildLog =~ /independent versioning is not supported/ +assert buildLog =~ /changesets:release/ + +// And it did NOT prompt (no InteractiveInputPrompt / "What is the release version" line). +assert !(buildLog =~ /What is the release version/) : \ + "The policy must not prompt; it should fall back to leaving the version unchanged." + +true diff --git a/changesets-maven-plugin/src/it/release/.changeset/VERSION b/changesets-maven-plugin/src/it/release/.changeset/VERSION deleted file mode 100644 index fad066f..0000000 --- a/changesets-maven-plugin/src/it/release/.changeset/VERSION +++ /dev/null @@ -1 +0,0 @@ -2.5.0 \ No newline at end of file diff --git a/changesets-maven-plugin/src/it/release/.changeset/VERSIONS b/changesets-maven-plugin/src/it/release/.changeset/VERSIONS new file mode 100644 index 0000000..a5f9aae --- /dev/null +++ b/changesets-maven-plugin/src/it/release/.changeset/VERSIONS @@ -0,0 +1 @@ +my-package=2.5.0 diff --git a/changesets-maven-plugin/src/it/release/verify.groovy b/changesets-maven-plugin/src/it/release/verify.groovy index 4068103..0fdf57e 100644 --- a/changesets-maven-plugin/src/it/release/verify.groovy +++ b/changesets-maven-plugin/src/it/release/verify.groovy @@ -2,14 +2,8 @@ import groovy.xml.XmlSlurper import static org.assertj.core.api.Assertions.assertThat -String expectedVersion = '2.5.0'; +String expectedVersion = '2.5.0' -// The VERSION file should contain the correct version number -assertThat(new File(basedir, '.changeset/VERSION')) - .content() - .isEqualTo(expectedVersion) - -// The root pom version should have the same value as the VERSION file def project = new XmlSlurper().parse(new File(basedir, 'pom.xml')) assertThat(project.version).isEqualTo(expectedVersion) diff --git a/changesets-maven-plugin/src/it/snapshot-versions/.changeset/config.json b/changesets-maven-plugin/src/it/snapshot-versions/.changeset/config.json new file mode 100644 index 0000000..d19875b --- /dev/null +++ b/changesets-maven-plugin/src/it/snapshot-versions/.changeset/config.json @@ -0,0 +1,3 @@ +{ + "versioning": "independent" +} diff --git a/changesets-maven-plugin/src/it/snapshot-versions/.changeset/patch-a.md b/changesets-maven-plugin/src/it/snapshot-versions/.changeset/patch-a.md new file mode 100644 index 0000000..21be9dc --- /dev/null +++ b/changesets-maven-plugin/src/it/snapshot-versions/.changeset/patch-a.md @@ -0,0 +1,6 @@ +--- +"module-a": patch +"module-b": minor +--- + +Patched a thing in module-a, minor change in module-b diff --git a/changesets-maven-plugin/src/it/snapshot-versions/invoker.properties b/changesets-maven-plugin/src/it/snapshot-versions/invoker.properties new file mode 100644 index 0000000..dce7e2c --- /dev/null +++ b/changesets-maven-plugin/src/it/snapshot-versions/invoker.properties @@ -0,0 +1 @@ +invoker.goals=${project.groupId}:${project.artifactId}:${project.version}:prepare diff --git a/changesets-maven-plugin/src/it/snapshot-versions/module-a/pom.xml b/changesets-maven-plugin/src/it/snapshot-versions/module-a/pom.xml new file mode 100644 index 0000000..d9a2817 --- /dev/null +++ b/changesets-maven-plugin/src/it/snapshot-versions/module-a/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-a + 1.1.3-SNAPSHOT + + se.fortnox.maven.it + snapshot-root + 1.0.0-SNAPSHOT + + diff --git a/changesets-maven-plugin/src/it/snapshot-versions/module-b/pom.xml b/changesets-maven-plugin/src/it/snapshot-versions/module-b/pom.xml new file mode 100644 index 0000000..2229f4e --- /dev/null +++ b/changesets-maven-plugin/src/it/snapshot-versions/module-b/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + module-b + 2.5.3-SNAPSHOT + + se.fortnox.maven.it + snapshot-root + 1.0.0-SNAPSHOT + + diff --git a/changesets-maven-plugin/src/it/snapshot-versions/pom.xml b/changesets-maven-plugin/src/it/snapshot-versions/pom.xml new file mode 100644 index 0000000..fcde892 --- /dev/null +++ b/changesets-maven-plugin/src/it/snapshot-versions/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + se.fortnox.maven.it + snapshot-root + 1.0.0-SNAPSHOT + pom + + IT: SNAPSHOT versions should be treated as the next release target — no double-bump. + + + UTF-8 + + + module-a + module-b + + diff --git a/changesets-maven-plugin/src/it/snapshot-versions/verify.groovy b/changesets-maven-plugin/src/it/snapshot-versions/verify.groovy new file mode 100644 index 0000000..47e54ce --- /dev/null +++ b/changesets-maven-plugin/src/it/snapshot-versions/verify.groovy @@ -0,0 +1,19 @@ +import groovy.xml.XmlSlurper + +import static org.assertj.core.api.Assertions.assertThat + +// SNAPSHOT pom = "next intended release". Patch confirms; minor/major escalate from non-boundary. +// module-a 1.1.3-SNAPSHOT + patch -> release 1.1.3 -> next dev 1.1.4-SNAPSHOT (no double-bump) +// module-b 2.5.3-SNAPSHOT + minor -> release 2.6.0 -> next dev 2.6.1-SNAPSHOT (escalates) + +def versions = new File(basedir, '.changeset/VERSIONS').text +assertThat(versions).contains("module-a=1.1.3") +assertThat(versions).contains("module-b=2.6.0") + +def moduleA = new XmlSlurper().parse(new File(basedir, 'module-a/pom.xml')) +assertThat(moduleA.version).isEqualTo('1.1.4-SNAPSHOT') + +def moduleB = new XmlSlurper().parse(new File(basedir, 'module-b/pom.xml')) +assertThat(moduleB.version).isEqualTo('2.6.1-SNAPSHOT') + +true diff --git a/changesets-maven-plugin/src/main/java/se/fortnox/changesets/VersionFile.java b/changesets-maven-plugin/src/main/java/se/fortnox/changesets/VersionFile.java deleted file mode 100644 index 2f00f1f..0000000 --- a/changesets-maven-plugin/src/main/java/se/fortnox/changesets/VersionFile.java +++ /dev/null @@ -1,45 +0,0 @@ -package se.fortnox.changesets; - -import org.apache.maven.project.MavenProject; -import org.codehaus.plexus.logging.Logger; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.Optional; - -import static se.fortnox.changesets.ChangesetWriter.CHANGESET_DIR; - -@Singleton -public class VersionFile { - private final Path versionFile; - - @Inject - public VersionFile(MavenProject project, Logger log) { - this.versionFile = project.getBasedir().toPath().resolve(CHANGESET_DIR).resolve("VERSION").toAbsolutePath(); - log.info("Using version file " + versionFile); - } - - public Optional currentVersion() { - if(!Files.exists(versionFile)) { - return Optional.empty(); - } - try { - return Optional.of(Files.readString(versionFile)); - } catch (IOException e) { - return Optional.empty(); - } - } - - public void assignVersion(String newVersion) { - try { - Files.writeString(versionFile, newVersion, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/changesets-maven-plugin/src/main/java/se/fortnox/changesets/VersionsFile.java b/changesets-maven-plugin/src/main/java/se/fortnox/changesets/VersionsFile.java new file mode 100644 index 0000000..b0f53d8 --- /dev/null +++ b/changesets-maven-plugin/src/main/java/se/fortnox/changesets/VersionsFile.java @@ -0,0 +1,65 @@ +package se.fortnox.changesets; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.TreeMap; + +import static se.fortnox.changesets.ChangesetWriter.CHANGESET_DIR; + +/** + * Read/write helper for {@code .changeset/VERSIONS} — the prepare→release handoff file + * keyed by artifactId. Only modules that bumped in a release are present. + */ +public class VersionsFile { + public static final String FILE = "VERSIONS"; + + public static Path locate(Path reactorRoot) { + return reactorRoot.resolve(CHANGESET_DIR).resolve(FILE); + } + + public static Map read(Path reactorRoot) { + Path file = locate(reactorRoot); + if (!Files.exists(file)) { + return Map.of(); + } + Properties props = new Properties(); + try (BufferedReader r = Files.newBufferedReader(file)) { + props.load(r); + } catch (IOException e) { + throw new RuntimeException("Failed to read " + file, e); + } + Map out = new LinkedHashMap<>(); + for (String key : props.stringPropertyNames()) { + out.put(key, props.getProperty(key)); + } + return out; + } + + public static Optional lookup(Path reactorRoot, String artifactId) { + return Optional.ofNullable(read(reactorRoot).get(artifactId)); + } + + public static void write(Path reactorRoot, Map versions) { + Path file = locate(reactorRoot); + try { + Files.createDirectories(file.getParent()); + try (BufferedWriter w = Files.newBufferedWriter(file)) { + for (Map.Entry e : new TreeMap<>(versions).entrySet()) { + w.write(e.getKey()); + w.write('='); + w.write(e.getValue()); + w.newLine(); + } + } + } catch (IOException e) { + throw new RuntimeException("Failed to write " + file, e); + } + } +} diff --git a/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/BomResolver.java b/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/BomResolver.java new file mode 100644 index 0000000..eba6243 --- /dev/null +++ b/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/BomResolver.java @@ -0,0 +1,54 @@ +package se.fortnox.changesets.maven; + +import org.apache.maven.model.Dependency; +import org.apache.maven.model.DependencyManagement; +import org.apache.maven.model.Model; +import org.apache.maven.project.MavenProject; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Walks a BOM module's raw {@code } to map managed reactor + * artifacts to the property name that pins their version. Used to rewrite the + * BOM's {@code } when sibling modules bump. + */ +public class BomResolver { + private static final Pattern PROPERTY_REF = Pattern.compile("\\$\\{([^}]+)}"); + + /** + * @param bom The BOM Maven project (its original model is consulted). + * @param reactorIds The set of reactor module {@code groupId:artifactId} keys. + * @return Ordered map of reactor artifactId → property name in the BOM that pins it. + * Only entries whose {@code } is a property reference are included. + */ + public static Map resolvePinnedProperties(MavenProject bom, Map reactorIds) { + Map result = new LinkedHashMap<>(); + Model model = bom.getOriginalModel(); + if (model == null) { + return result; + } + DependencyManagement dm = model.getDependencyManagement(); + if (dm == null) { + return result; + } + for (Dependency dep : dm.getDependencies()) { + String key = dep.getGroupId() + ":" + dep.getArtifactId(); + String reactorArtifactId = reactorIds.get(key); + if (reactorArtifactId == null) { + continue; + } + String version = dep.getVersion(); + if (version == null) { + continue; + } + Matcher m = PROPERTY_REF.matcher(version); + if (m.matches()) { + result.put(reactorArtifactId, m.group(1)); + } + } + return result; + } +} diff --git a/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/PomUpdater.java b/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/PomUpdater.java index aa2ec01..421cf0e 100644 --- a/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/PomUpdater.java +++ b/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/PomUpdater.java @@ -39,6 +39,23 @@ public static void setProjectVersion(File outFile, String newVersion) { } + /** + * Set a property value in the {@code } section of a pom file. + * + * @param outFile The pom file to update + * @param property The property name + * @param value The new property value + */ + public static void setProperty(File outFile, String property, String value) { + updatePom(outFile, newPom -> { + try { + PomHelper.setPropertyVersion(newPom, null, property, value); + } catch (XMLStreamException e) { + LOG.error("Failed to update property {} in {}", property, outFile, e); + } + }); + } + /** * Set parent project version in a pom file * diff --git a/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/PrepareMojo.java b/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/PrepareMojo.java index d4a7f47..b0eee3d 100644 --- a/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/PrepareMojo.java +++ b/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/PrepareMojo.java @@ -1,95 +1,277 @@ package se.fortnox.changesets.maven; +import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.logging.Logger; -import org.semver4j.Semver; +import se.fortnox.changesets.BumpPlanner; +import se.fortnox.changesets.BumpPlanner.ModuleBump; import se.fortnox.changesets.ChangelogAggregator; +import se.fortnox.changesets.ChangelogAggregator.BomContext; +import se.fortnox.changesets.ChangelogAggregator.ReleaseEntry; import se.fortnox.changesets.Changeset; import se.fortnox.changesets.ChangesetLocator; +import se.fortnox.changesets.ChangesetsConfig; +import se.fortnox.changesets.ChangesetsConfig.Bom; import se.fortnox.changesets.VersionCalculator; -import se.fortnox.changesets.VersionFile; +import se.fortnox.changesets.VersionsFile; import javax.inject.Inject; import java.io.File; import java.nio.file.Path; +import java.util.LinkedHashMap; import java.util.List; -import java.util.Optional; +import java.util.Map; + +import static se.fortnox.changesets.ChangesetWriter.CHANGESET_DIR; /** - * Applies all changesets into the changelog and calculates the new version number. + * Applies all changesets into the changelog, calculates new versions per module, and updates + * submodule poms. Runs once at the reactor root. *

- * This would normally be the last step in preparing a release PR. + * Versioning strategy is read from {@code .changeset/config.json}; defaults to {@code fixed}. */ -@Mojo(name = "prepare", defaultPhase = LifecyclePhase.INITIALIZE) +@Mojo(name = "prepare", defaultPhase = LifecyclePhase.INITIALIZE, aggregator = true) public class PrepareMojo extends AbstractMojo { - private final org.apache.maven.project.MavenProject project; - private final VersionFile versionFile; + private final MavenProject project; + private final MavenSession session; private final Logger logger; @Inject - public PrepareMojo(MavenProject project, VersionFile versionFile, Logger logger) { + public PrepareMojo(MavenProject project, MavenSession session, Logger logger) { this.project = project; - this.versionFile = versionFile; + this.session = session; this.logger = logger; } - /* + /** * Set to true in order to just process changeset files, avoiding any changes to the POM(s). */ @Parameter(property = "useReleasePluginIntegration", defaultValue = "false") protected boolean useReleasePluginIntegration = false; - public void execute() { - Path baseDir = project.getBasedir().toPath(); - String packageName = project.getArtifactId(); + /** + * Per-invocation override for BOM behavior. When {@code true}, any {@code bom} + * configuration in {@code .changeset/config.json} is ignored for this run — + * the BOM is not auto-bumped, its {@code } are not rewritten, and + * the changelog is rendered in plain multi-module mode (no consumer-parent + * wrapper header). Use this when you want to release a starter or two without + * cutting a new BOM version. + */ + @Parameter(property = "skipBom", defaultValue = "false") + protected boolean skipBom = false; + + public void execute() throws MojoExecutionException { + Path reactorRoot = project.getBasedir().toPath(); + Path changesetDir = reactorRoot.resolve(CHANGESET_DIR); - // Get all relevant changesets - ChangesetLocator changesetLocator = new ChangesetLocator(baseDir); - List changesets = changesetLocator.getChangesets(packageName); + ChangesetsConfig loadedConfig = ChangesetsConfig.load(changesetDir); + ChangesetsConfig config = applySkipBom(loadedConfig); + logger.info("Versioning strategy: " + config.versioning()); + Map byArtifactId = collectProjectsByArtifactId(); + validateBomConfig(config.bom(), byArtifactId); + + List changesets = new ChangesetLocator(reactorRoot).getAllChangesets(); if (changesets.isEmpty()) { - logger.info("No changesets for package: " + packageName + " found in " + baseDir); + logger.info("No changesets found in " + changesetDir); + return; + } + + Map reactor = collectReactorVersions(); + Map plan = BumpPlanner.plan(changesets, reactor, config); + + if (plan.isEmpty()) { + logger.info("No changesets matched any reactor module"); return; } - // Calculate new version - String currentVersion = versionFile.currentVersion().orElse("0.0.0");; - String newVersion = VersionCalculator.getNewVersion(currentVersion, changesets); - logger.info("Old version was " + currentVersion + ", will be updated to " + newVersion); + Map changedVersions = new LinkedHashMap<>(); + plan.values().stream() + .filter(ModuleBump::isVersionChange) + .forEach(bump -> changedVersions.put(bump.artifactId(), bump.newVersion())); - // Move changesets into CHANGELOG.md - ChangelogAggregator changelogAggregator = new ChangelogAggregator(baseDir); - changelogAggregator.mergeChangesetsToChangelog(packageName, newVersion); + if (!changedVersions.isEmpty()) { + VersionsFile.write(reactorRoot, changedVersions); + logger.info("Wrote " + VersionsFile.locate(reactorRoot) + " with " + changedVersions.size() + " entry/entries"); + } - // Advance to version deduced from changesets - versionFile.assignVersion(newVersion); + writeChangelog(reactorRoot, plan, config.bom(), byArtifactId); - if(useReleasePluginIntegration) { + if (useReleasePluginIntegration) { logger.info("Changesets processed, but not updating POMs due to useReleasePluginIntegration being set to true."); return; } - // Set newVersion property to be used by versions:set - if (!newVersion.equals(currentVersion)) { - String pomVersion = Optional.ofNullable(Semver.coerce(newVersion)) - .map(semver -> semver.withIncPatch().withPreRelease("SNAPSHOT").getVersion()) - .orElseThrow(() -> new IllegalArgumentException("Cannot coerce \"%s\" into a semantic version.".formatted(currentVersion))); + Map snapshotVersions = applyPomVersions(plan, byArtifactId); + if (config.bom() != null) { + applyBomPropertyUpdates(config.bom(), plan, snapshotVersions, byArtifactId); + } + } + + private ChangesetsConfig applySkipBom(ChangesetsConfig config) { + if (!skipBom || config.bom() == null) { + return config; + } + logger.info("skipBom=true: ignoring BOM config '" + config.bom().module() + "' for this prepare run"); + return new ChangesetsConfig(config.versioning(), config.linked(), config.fixed(), config.changelog(), null); + } + + private Map collectProjectsByArtifactId() { + Map byArtifactId = new LinkedHashMap<>(); + for (MavenProject p : session.getProjects()) { + byArtifactId.put(p.getArtifactId(), p); + } + return byArtifactId; + } + + private void validateBomConfig(Bom bom, Map byArtifactId) throws MojoExecutionException { + if (bom == null) { + return; + } + if (!byArtifactId.containsKey(bom.module())) { + throw new MojoExecutionException( + "bom.module '" + bom.module() + "' is not present in the reactor"); + } + if (bom.consumerParent() != null) { + MavenProject cp = byArtifactId.get(bom.consumerParent()); + if (cp == null) { + throw new MojoExecutionException( + "bom.consumerParent '" + bom.consumerParent() + "' is not present in the reactor"); + } + String ownVersion = cp.getOriginalModel() == null ? null : cp.getOriginalModel().getVersion(); + MavenProject bomProject = byArtifactId.get(bom.module()); + String bomVersion = bomProject.getOriginalModel() == null ? null : bomProject.getOriginalModel().getVersion(); + if (ownVersion != null && !ownVersion.equals(bomVersion)) { + throw new MojoExecutionException( + "bom.consumerParent '" + bom.consumerParent() + "' has its own (" + + ownVersion + ") different from the BOM's (" + bomVersion + "); " + + "consumer-parent must inherit its version from the BOM"); + } + } + } + + private Map collectReactorVersions() { + Map reactor = new LinkedHashMap<>(); + for (MavenProject p : session.getProjects()) { + reactor.put(p.getArtifactId(), p.getVersion() == null ? "0.0.0" : p.getVersion()); + } + return reactor; + } + + private void writeChangelog(Path reactorRoot, Map plan, Bom bom, Map byArtifactId) { + Map entries = new LinkedHashMap<>(); + for (ModuleBump bump : plan.values()) { + entries.put(bump.artifactId(), new ReleaseEntry( + bump.artifactId(), + bump.newVersion(), + bump.changesets() + )); + } + + BomContext bomContext = null; + if (bom != null && plan.containsKey(bom.module())) { + ModuleBump bomBump = plan.get(bom.module()); + String headerArtifactId = bom.consumerParent() != null ? bom.consumerParent() : bom.module(); + Map pinnedUpdates = collectPinnedUpdates(bom, plan, byArtifactId); + bomContext = new BomContext(headerArtifactId, bomBump.newVersion(), bom.module(), pinnedUpdates); + } + + new ChangelogAggregator(reactorRoot).mergeReleaseToChangelog(entries, bomContext); + } + private Map collectPinnedUpdates(Bom bom, Map plan, Map byArtifactId) { + MavenProject bomProject = byArtifactId.get(bom.module()); + Map reactorIdsByGav = new LinkedHashMap<>(); + for (MavenProject p : session.getProjects()) { + reactorIdsByGav.put(p.getGroupId() + ":" + p.getArtifactId(), p.getArtifactId()); + } + Map pinnedProps = BomResolver.resolvePinnedProperties(bomProject, reactorIdsByGav); - logger.info("Updating " + project.getFile() + " to " + pomVersion); - PomUpdater.setProjectVersion(project.getFile(), pomVersion); + Map updates = new LinkedHashMap<>(); + for (String artifactId : pinnedProps.keySet()) { + ModuleBump bump = plan.get(artifactId); + if (bump != null && bump.isVersionChange()) { + updates.put(artifactId, bump.newVersion()); + } + } + return updates; + } + + /** + * Writes each bumped module's pom to its next development (snapshot) version, and + * keeps parent references in sync when their parent is also bumped. Returns the + * artifactId → snapshotVersion map for downstream use (e.g. BOM property rewrite). + */ + private Map applyPomVersions(Map plan, Map byArtifactId) { + Map snapshotVersions = new LinkedHashMap<>(); + + for (ModuleBump bump : plan.values()) { + if (!bump.isVersionChange()) { + continue; + } + MavenProject moduleProject = byArtifactId.get(bump.artifactId()); + if (moduleProject == null) { + continue; + } + String snapshotVersion = VersionCalculator.nextDevelopmentVersion(bump.newVersion()); + snapshotVersions.put(bump.artifactId(), snapshotVersion); + File pomFile = moduleProject.getFile(); + logger.info("Updating " + pomFile + " to " + snapshotVersion); + PomUpdater.setProjectVersion(pomFile, snapshotVersion); + } + + syncParentReferences(snapshotVersions, byArtifactId); + return snapshotVersions; + } + + private void syncParentReferences(Map snapshotVersions, Map byArtifactId) { + for (MavenProject p : session.getProjects()) { + if (p.getOriginalModel() == null || p.getOriginalModel().getParent() == null) { + continue; + } + String parentArtifactId = p.getOriginalModel().getParent().getArtifactId(); + String parentSnapshot = snapshotVersions.get(parentArtifactId); + if (parentSnapshot == null) { + continue; + } + File pomFile = p.getFile(); + logger.info("Updating " + pomFile + " parent ref to " + parentSnapshot); + PomUpdater.setProjectParentVersion(pomFile, parentSnapshot); + } + } + + private void applyBomPropertyUpdates( + Bom bom, + Map plan, + Map snapshotVersions, + Map byArtifactId + ) { + MavenProject bomProject = byArtifactId.get(bom.module()); + Map reactorIdsByGav = new LinkedHashMap<>(); + for (MavenProject p : session.getProjects()) { + reactorIdsByGav.put(p.getGroupId() + ":" + p.getArtifactId(), p.getArtifactId()); + } + Map pinnedProps = BomResolver.resolvePinnedProperties(bomProject, reactorIdsByGav); - // Update submodules to reference the parent project with the new version - List modules = project.getModules(); - modules.forEach(module -> { - File modulePom = baseDir.resolve(module).resolve("pom.xml").toFile(); - logger.info("Updating submodule" + modulePom + " to " + pomVersion); - PomUpdater.setProjectParentVersion(modulePom, pomVersion); - }); + File bomPom = bomProject.getFile(); + for (Map.Entry entry : pinnedProps.entrySet()) { + String artifactId = entry.getKey(); + String propertyName = entry.getValue(); + ModuleBump bump = plan.get(artifactId); + if (bump == null || !bump.isVersionChange()) { + continue; + } + String snapshotVersion = snapshotVersions.get(artifactId); + if (snapshotVersion == null) { + continue; + } + logger.info("Updating " + bomPom + " property " + propertyName + " to " + snapshotVersion); + PomUpdater.setProperty(bomPom, propertyName, snapshotVersion); } } } diff --git a/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/ReleaseMojo.java b/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/ReleaseMojo.java index fdaf90a..49adbe2 100644 --- a/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/ReleaseMojo.java +++ b/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/ReleaseMojo.java @@ -1,43 +1,107 @@ package se.fortnox.changesets.maven; +import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.logging.Logger; -import se.fortnox.changesets.VersionFile; +import se.fortnox.changesets.ChangesetsConfig; +import se.fortnox.changesets.ChangesetsConfig.Bom; +import se.fortnox.changesets.VersionsFile; import javax.inject.Inject; import java.io.File; import java.nio.file.Path; -import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; + +import static se.fortnox.changesets.ChangesetWriter.CHANGESET_DIR; @Mojo(name = "release", defaultPhase = LifecyclePhase.INITIALIZE, aggregator = true) public class ReleaseMojo extends AbstractMojo { private final MavenProject project; - private final VersionFile versionFile; + private final MavenSession session; private final Logger log; @Inject - public ReleaseMojo(MavenProject project, VersionFile versionFile, Logger log) { + public ReleaseMojo(MavenProject project, MavenSession session, Logger log) { this.project = project; - this.versionFile = versionFile; + this.session = session; this.log = log; } public void execute() { - Path baseDir = project.getBasedir().toPath(); - String pomVersion = versionFile.currentVersion().orElse("0.0.0"); - - log.info("Updating " + project.getFile() + " to " + pomVersion); - PomUpdater.setProjectVersion(project.getFile(), pomVersion); - - // Update submodules to reference the parent project with the new version - List modules = project.getModules(); - modules.forEach(module -> { - File modulePom = baseDir.resolve(module).resolve("pom.xml").toFile(); - log.info("Updating submodule" + modulePom + " to " + pomVersion); - PomUpdater.setProjectParentVersion(modulePom, pomVersion); - }); + Path reactorRoot = project.getBasedir().toPath(); + Map versions = VersionsFile.read(reactorRoot); + if (versions.isEmpty()) { + log.info("No release versions found in " + VersionsFile.locate(reactorRoot)); + return; + } + + Map byArtifactId = new LinkedHashMap<>(); + for (MavenProject p : session.getProjects()) { + byArtifactId.put(p.getArtifactId(), p); + } + + for (Map.Entry entry : versions.entrySet()) { + MavenProject moduleProject = byArtifactId.get(entry.getKey()); + if (moduleProject == null) { + log.warn("VERSIONS entry for unknown module: " + entry.getKey()); + continue; + } + File pomFile = moduleProject.getFile(); + log.info("Updating " + pomFile + " to " + entry.getValue()); + PomUpdater.setProjectVersion(pomFile, entry.getValue()); + } + + syncParentReferences(versions, byArtifactId); + + ChangesetsConfig config = ChangesetsConfig.load(reactorRoot.resolve(CHANGESET_DIR)); + if (config.bom() != null && versions.containsKey(config.bom().module())) { + applyBomPropertyUpdates(config.bom(), versions, byArtifactId); + } else if (config.bom() != null) { + log.info("BOM '" + config.bom().module() + "' not in VERSIONS — skipping BOM property updates"); + } + } + + private void syncParentReferences(Map versions, Map byArtifactId) { + for (MavenProject p : session.getProjects()) { + if (p.getOriginalModel() == null || p.getOriginalModel().getParent() == null) { + continue; + } + String parentArtifactId = p.getOriginalModel().getParent().getArtifactId(); + String parentReleaseVersion = versions.get(parentArtifactId); + if (parentReleaseVersion == null) { + continue; + } + File pomFile = p.getFile(); + log.info("Updating " + pomFile + " parent ref to " + parentReleaseVersion); + PomUpdater.setProjectParentVersion(pomFile, parentReleaseVersion); + } + } + + private void applyBomPropertyUpdates(Bom bom, Map versions, Map byArtifactId) { + MavenProject bomProject = byArtifactId.get(bom.module()); + if (bomProject == null) { + return; + } + Map reactorIdsByGav = new LinkedHashMap<>(); + for (MavenProject p : session.getProjects()) { + reactorIdsByGav.put(p.getGroupId() + ":" + p.getArtifactId(), p.getArtifactId()); + } + Map pinnedProps = BomResolver.resolvePinnedProperties(bomProject, reactorIdsByGav); + + File bomPom = bomProject.getFile(); + for (Map.Entry entry : pinnedProps.entrySet()) { + String artifactId = entry.getKey(); + String propertyName = entry.getValue(); + String releaseVersion = versions.get(artifactId); + if (releaseVersion == null) { + continue; + } + log.info("Updating " + bomPom + " property " + propertyName + " to " + releaseVersion); + PomUpdater.setProperty(bomPom, propertyName, releaseVersion); + } } } diff --git a/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/policy/ChangesetsVersionPolicy.java b/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/policy/ChangesetsVersionPolicy.java index 9b4082c..1b37e93 100644 --- a/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/policy/ChangesetsVersionPolicy.java +++ b/changesets-maven-plugin/src/main/java/se/fortnox/changesets/maven/policy/ChangesetsVersionPolicy.java @@ -1,5 +1,6 @@ package se.fortnox.changesets.maven.policy; +import org.apache.maven.execution.MavenSession; import org.apache.maven.project.MavenProject; import org.apache.maven.shared.release.policy.PolicyException; import org.apache.maven.shared.release.policy.version.VersionPolicy; @@ -9,53 +10,174 @@ import org.codehaus.plexus.component.annotations.Component; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import se.fortnox.changesets.VersionsFile; import javax.inject.Inject; -import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import static se.fortnox.changesets.ChangesetWriter.CHANGESET_DIR; import static se.fortnox.changesets.VersionCalculator.nextDevelopmentVersion; +/** + * VersionPolicy for the maven-release-plugin that resolves each module's release/next-dev version + * from {@code .changeset/VERSIONS} (written by {@code changesets:prepare}). + *

+ * The maven-release-plugin's {@code VersionPolicyRequest} only carries {@code version} and the + * reactor {@code workingDirectory} — no artifactId — so this policy identifies the calling module + * by matching {@code request.getVersion()} first against reactor pom versions (map-release-versions + * phase) and then, if that fails, against VERSIONS values (map-development-versions phase, where + * release-plugin passes the just-computed release version). If exactly one candidate is found, its + * artifactId keys into VERSIONS. On ambiguous matches, the policy accepts the ambiguity if all + * candidates share the same target; otherwise it leaves the version unchanged with an actionable + * warning. + */ @Component(role = VersionPolicy.class, hint = "changesets", - description = "A VersionPolicy implementation that uses changesets-java to calculate the current and next version.") + description = "A VersionPolicy implementation that uses changesets-java to calculate the current and next version per Maven module.") public class ChangesetsVersionPolicy implements VersionPolicy { private final Logger logger = LoggerFactory.getLogger(getClass()); - public static final String CHANGESET_DIR = ".changeset"; private final MavenProject project; + private final MavenSession session; @Inject - public ChangesetsVersionPolicy(MavenProject project) { + public ChangesetsVersionPolicy(MavenProject project, MavenSession session) { this.project = project; + this.session = session; } @Override - public VersionPolicyResult getReleaseVersion(VersionPolicyRequest versionPolicyRequest) throws PolicyException, VersionParseException { - Path versionFile = project.getBasedir().toPath().resolve(CHANGESET_DIR).resolve("VERSION"); - logger.info("Reading version from " + versionFile); - try { - String version = Files.readString(versionFile); - return new VersionPolicyResult() - .setVersion(version); - } catch (IOException e) { - throw new RuntimeException(e); + public VersionPolicyResult getReleaseVersion(VersionPolicyRequest request) throws PolicyException, VersionParseException { + return new VersionPolicyResult().setVersion( + lookup(request) + .map(ChangesetsVersionPolicy::stripSnapshot) + .orElseGet(() -> stripSnapshot(request.getVersion()))); + } + + @Override + public VersionPolicyResult getDevelopmentVersion(VersionPolicyRequest request) throws PolicyException, VersionParseException { + return new VersionPolicyResult().setVersion( + lookup(request) + .map(v -> nextDevelopmentVersion(stripSnapshot(v))) + // Unmapped: return a SNAPSHOT so release-plugin's dev-version loop terminates. + // If the request is already a SNAPSHOT (typical: unmapped module retaining its + // pre-release version), leave it as-is. If it's not (typical: release-plugin's + // dev-version phase passing us the just-computed release version for a module + // we can no longer identify), bump to next-dev. + .orElseGet(() -> ensureSnapshot(request.getVersion()))); + } + + private static String ensureSnapshot(String version) { + if (version == null) { + return nextDevelopmentVersion("0.0.0"); } + return version.endsWith("-SNAPSHOT") ? version : nextDevelopmentVersion(version); + } + private Optional lookup(VersionPolicyRequest request) { + Optional reactorRoot = findReactorRoot(reactorRootHint(request)); + if (reactorRoot.isEmpty()) { + logger.info("No .changeset dir found; using current version {}", request.getVersion()); + return Optional.empty(); + } + Map versions = VersionsFile.read(reactorRoot.get()); + List candidates = candidateArtifactIds(request, versions); + if (candidates.isEmpty()) { + logger.info("No VERSIONS match for version {}; leaving version unchanged", request.getVersion()); + return Optional.empty(); + } + if (candidates.size() == 1) { + String mapped = versions.get(candidates.get(0)); + logger.info("Resolved release version for {} from VERSIONS: {}", candidates.get(0), mapped); + return Optional.of(mapped); + } + // Ambiguous: multiple reactor modules share the same current version. If they all map to + // the same target in VERSIONS (typical for BOM / fixed / linked groups that bump together), + // that target is unambiguous even if the artifactId is not. + Set distinctTargets = candidates.stream().map(versions::get).collect(Collectors.toSet()); + if (distinctTargets.size() == 1) { + String mapped = distinctTargets.iterator().next(); + logger.info("Ambiguous artifact for version {} but all candidates map to {}; using it. Candidates: {}", + request.getVersion(), mapped, candidates); + return Optional.of(mapped); + } + logger.warn( + "Ambiguous VERSIONS lookup for version {}: candidates {} map to differing targets {}; " + + "leaving version unchanged. Combining maven-release-plugin with independent versioning " + + "is not supported — use `changesets:prepare` + `changesets:release` instead.", + request.getVersion(), candidates, distinctTargets); + return Optional.empty(); + } + /** + * Reactor artifactIds present in VERSIONS that could correspond to the requested version. + *

+ * Tries two matches, in order of specificity: + *

    + *
  1. Reactor project's current pom version equals the requested version — the normal case + * during the map-release-versions phase.
  2. + *
  3. VERSIONS value equals the requested version — needed for the map-development-versions + * phase, where release-plugin passes us the just-computed release version (which is the + * VERSIONS value from the prior phase) rather than the module's current pom version.
  4. + *
+ */ + private List candidateArtifactIds(VersionPolicyRequest request, Map versions) { + if (session == null || session.getProjects() == null) { + return List.of(); + } + String requested = request.getVersion(); + if (requested == null) { + return List.of(); + } + Map byArtifactId = new LinkedHashMap<>(); + for (MavenProject p : session.getProjects()) { + byArtifactId.put(p.getArtifactId(), p); + } + List byCurrentPomVersion = versions.keySet().stream() + .filter(byArtifactId::containsKey) + .filter(a -> requested.equals(byArtifactId.get(a).getVersion())) + .toList(); + if (!byCurrentPomVersion.isEmpty()) { + return byCurrentPomVersion; + } + return versions.entrySet().stream() + .filter(e -> requested.equals(e.getValue())) + .map(Map.Entry::getKey) + .toList(); } - @Override - public VersionPolicyResult getDevelopmentVersion(VersionPolicyRequest versionPolicyRequest) throws PolicyException, VersionParseException { - Path versionFile = project.getBasedir().toPath().resolve(CHANGESET_DIR).resolve("VERSION"); - try { - String version = Files.readString(versionFile); - return new VersionPolicyResult() - .setVersion(nextDevelopmentVersion(version)); - } catch (IOException e) { - throw new RuntimeException(e); + private Path reactorRootHint(VersionPolicyRequest request) { + String wd = request.getWorkingDirectory(); + if (wd != null && !wd.isBlank()) { + return Path.of(wd); } + return project.getBasedir().toPath(); + } + private static Optional findReactorRoot(Path start) { + Path current = start.toAbsolutePath(); + while (current != null) { + if (Files.isDirectory(current.resolve(CHANGESET_DIR))) { + return Optional.of(current); + } + current = current.getParent(); + } + return Optional.empty(); + } + + private static String stripSnapshot(String version) { + if (version == null) { + return "0.0.0"; + } + return version.endsWith("-SNAPSHOT") + ? version.substring(0, version.length() - "-SNAPSHOT".length()) + : version; } } diff --git a/changesets-maven-plugin/src/test/java/se/fortnox/changesets/maven/BomResolverTest.java b/changesets-maven-plugin/src/test/java/se/fortnox/changesets/maven/BomResolverTest.java new file mode 100644 index 0000000..c8030f9 --- /dev/null +++ b/changesets-maven-plugin/src/test/java/se/fortnox/changesets/maven/BomResolverTest.java @@ -0,0 +1,170 @@ +package se.fortnox.changesets.maven; + +import org.apache.maven.model.Dependency; +import org.apache.maven.model.DependencyManagement; +import org.apache.maven.model.Model; +import org.apache.maven.project.MavenProject; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +class BomResolverTest { + + @Test + void mapsReactorArtifactsToPinningProperties() { + Model model = new Model(); + DependencyManagement dm = new DependencyManagement(); + dm.addDependency(dep("com.example", "module-a", "${module-a.version}")); + dm.addDependency(dep("com.example", "module-b", "${module-b.version}")); + model.setDependencyManagement(dm); + MavenProject bom = new MavenProject(); + bom.setOriginalModel(model); + + Map result = BomResolver.resolvePinnedProperties(bom, reactorIds( + "com.example:module-a", "module-a", + "com.example:module-b", "module-b" + )); + + assertThat(result).containsExactly( + entry("module-a", "module-a.version"), + entry("module-b", "module-b.version") + ); + } + + @Test + void ignoresDependenciesNotInReactor() { + Model model = new Model(); + DependencyManagement dm = new DependencyManagement(); + dm.addDependency(dep("com.example", "module-a", "${module-a.version}")); + dm.addDependency(dep("org.thirdparty", "some-lib", "${thirdparty.version}")); + model.setDependencyManagement(dm); + MavenProject bom = new MavenProject(); + bom.setOriginalModel(model); + + Map result = BomResolver.resolvePinnedProperties(bom, reactorIds( + "com.example:module-a", "module-a" + )); + + assertThat(result).containsOnlyKeys("module-a"); + } + + @Test + void ignoresDependenciesWithLiteralVersion() { + Model model = new Model(); + DependencyManagement dm = new DependencyManagement(); + dm.addDependency(dep("com.example", "module-a", "1.2.3")); + dm.addDependency(dep("com.example", "module-b", "${module-b.version}")); + model.setDependencyManagement(dm); + MavenProject bom = new MavenProject(); + bom.setOriginalModel(model); + + Map result = BomResolver.resolvePinnedProperties(bom, reactorIds( + "com.example:module-a", "module-a", + "com.example:module-b", "module-b" + )); + + assertThat(result).containsOnlyKeys("module-b"); + } + + @Test + void ignoresDependenciesWithoutVersion() { + Model model = new Model(); + DependencyManagement dm = new DependencyManagement(); + Dependency d = new Dependency(); + d.setGroupId("com.example"); + d.setArtifactId("module-a"); + // no version set + dm.addDependency(d); + model.setDependencyManagement(dm); + MavenProject bom = new MavenProject(); + bom.setOriginalModel(model); + + Map result = BomResolver.resolvePinnedProperties(bom, reactorIds( + "com.example:module-a", "module-a" + )); + + assertThat(result).isEmpty(); + } + + @Test + void returnsEmptyWhenDependencyManagementMissing() { + Model model = new Model(); + MavenProject bom = new MavenProject(); + bom.setOriginalModel(model); + + Map result = BomResolver.resolvePinnedProperties(bom, reactorIds( + "com.example:module-a", "module-a" + )); + + assertThat(result).isEmpty(); + } + + @Test + void returnsEmptyWhenOriginalModelMissing() { + MavenProject bom = new MavenProject(); + // originalModel not set + + Map result = BomResolver.resolvePinnedProperties(bom, reactorIds( + "com.example:module-a", "module-a" + )); + + assertThat(result).isEmpty(); + } + + @Test + void ignoresPropertyRefEmbeddedInLargerVersionString() { + // only bare `${prop}` versions get treated as pinning + Model model = new Model(); + DependencyManagement dm = new DependencyManagement(); + dm.addDependency(dep("com.example", "module-a", "${module-a.version}.RELEASE")); + model.setDependencyManagement(dm); + MavenProject bom = new MavenProject(); + bom.setOriginalModel(model); + + Map result = BomResolver.resolvePinnedProperties(bom, reactorIds( + "com.example:module-a", "module-a" + )); + + assertThat(result).isEmpty(); + } + + @Test + void preservesInsertionOrder() { + Model model = new Model(); + DependencyManagement dm = new DependencyManagement(); + dm.addDependency(dep("com.example", "module-c", "${c.version}")); + dm.addDependency(dep("com.example", "module-a", "${a.version}")); + dm.addDependency(dep("com.example", "module-b", "${b.version}")); + model.setDependencyManagement(dm); + MavenProject bom = new MavenProject(); + bom.setOriginalModel(model); + + Map result = BomResolver.resolvePinnedProperties(bom, reactorIds( + "com.example:module-a", "module-a", + "com.example:module-b", "module-b", + "com.example:module-c", "module-c" + )); + + assertThat(result.keySet()).containsExactly("module-c", "module-a", "module-b"); + } + + private static Dependency dep(String groupId, String artifactId, String version) { + Dependency d = new Dependency(); + d.setGroupId(groupId); + d.setArtifactId(artifactId); + d.setVersion(version); + return d; + } + + private static Map reactorIds(String... kv) { + Map map = new LinkedHashMap<>(); + for (int i = 0; i < kv.length; i += 2) { + map.put(kv[i], kv[i + 1]); + } + return map; + } +} diff --git a/changesets-maven-plugin/src/test/java/se/fortnox/changesets/maven/policy/ChangesetsVersionPolicyTest.java b/changesets-maven-plugin/src/test/java/se/fortnox/changesets/maven/policy/ChangesetsVersionPolicyTest.java new file mode 100644 index 0000000..0fc58ee --- /dev/null +++ b/changesets-maven-plugin/src/test/java/se/fortnox/changesets/maven/policy/ChangesetsVersionPolicyTest.java @@ -0,0 +1,264 @@ +package se.fortnox.changesets.maven.policy; + +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Model; +import org.apache.maven.project.MavenProject; +import org.apache.maven.shared.release.policy.version.VersionPolicyRequest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ChangesetsVersionPolicyTest { + + @TempDir + Path reactorRoot; + + @Nested + class GetReleaseVersion { + + @Test + void returnsVersionFromVersionsFileStrippingSnapshot() throws Exception { + writeVersions(""" + module-a=1.2.3 + module-b=2.0.0 + """); + MavenProject rootProject = project("root", "1.0.0", reactorRoot); + MavenProject moduleA = project("module-a", "1.0.0-SNAPSHOT", reactorRoot.resolve("module-a")); + MavenProject moduleB = project("module-b", "2.0.0-SNAPSHOT", reactorRoot.resolve("module-b")); + + var result = policy(rootProject, session(List.of(rootProject, moduleA, moduleB))) + .getReleaseVersion(request("1.0.0-SNAPSHOT")); + + assertThat(result.getVersion()).isEqualTo("1.2.3"); + } + + @Test + void fallsBackToRequestVersionWhenModuleNotInVersionsFile() throws Exception { + writeVersions("other-module=9.9.9\n"); + MavenProject rootProject = project("root", "1.0.0", reactorRoot); + MavenProject moduleA = project("module-a", "1.4.6-SNAPSHOT", reactorRoot.resolve("module-a")); + + var result = policy(rootProject, session(List.of(rootProject, moduleA))) + .getReleaseVersion(request("1.4.6-SNAPSHOT")); + + assertThat(result.getVersion()).isEqualTo("1.4.6"); + } + + @Test + void fallsBackToRequestVersionWhenVersionsFileMissing() throws Exception { + // no VERSIONS file created + Files.createDirectories(reactorRoot.resolve(".changeset")); + MavenProject rootProject = project("root", "2.0.1-SNAPSHOT", reactorRoot); + + var result = policy(rootProject, session(List.of(rootProject))) + .getReleaseVersion(request("2.0.1-SNAPSHOT")); + + assertThat(result.getVersion()).isEqualTo("2.0.1"); + } + + @Test + void singleModuleFlowResolvesWhenReactorHasOnlyRoot() throws Exception { + writeVersions("my-package=3.1.4\n"); + MavenProject root = project("my-package", "3.0.0-SNAPSHOT", reactorRoot); + + var result = policy(root, session(List.of(root))) + .getReleaseVersion(request("3.0.0-SNAPSHOT")); + + assertThat(result.getVersion()).isEqualTo("3.1.4"); + } + } + + @Nested + class GetDevelopmentVersion { + + @Test + void returnsNextPatchSnapshotOfMappedVersion() throws Exception { + writeVersions("module-a=1.2.3\n"); + MavenProject root = project("root", "1.0.0", reactorRoot); + MavenProject moduleA = project("module-a", "1.0.0-SNAPSHOT", reactorRoot.resolve("module-a")); + + var result = policy(root, session(List.of(root, moduleA))) + .getDevelopmentVersion(request("1.0.0-SNAPSHOT")); + + assertThat(result.getVersion()).isEqualTo("1.2.4-SNAPSHOT"); + } + + @Test + void leavesUnmappedModuleUnchanged() throws Exception { + // Modules absent from VERSIONS are not bumped, so multi-module reactors + // don't inadvertently push a next-dev bump onto an aggregator root. + writeVersions("other=1.0.0\n"); + MavenProject root = project("root", "1.0.0", reactorRoot); + MavenProject moduleA = project("module-a", "2.5.0-SNAPSHOT", reactorRoot.resolve("module-a")); + + var result = policy(root, session(List.of(root, moduleA))) + .getDevelopmentVersion(request("2.5.0-SNAPSHOT")); + + assertThat(result.getVersion()).isEqualTo("2.5.0-SNAPSHOT"); + } + } + + @Nested + class ModuleIdentification { + + @Test + void identifiesModuleByCurrentVersionAcrossReactor() throws Exception { + // The injected MavenProject is the aggregator root — the policy must resolve + // the *per-module* artifactId from the reactor by matching request.getVersion(). + writeVersions(""" + module-a=2.1.0 + module-b=3.0.5 + """); + MavenProject root = project("root", "1.0.0", reactorRoot); + MavenProject moduleA = project("module-a", "2.0.5-SNAPSHOT", reactorRoot.resolve("module-a")); + MavenProject moduleB = project("module-b", "3.0.5-SNAPSHOT", reactorRoot.resolve("module-b")); + MavenSession session = session(List.of(root, moduleA, moduleB)); + + var forA = policy(root, session).getReleaseVersion(request("2.0.5-SNAPSHOT")); + var forB = policy(root, session).getReleaseVersion(request("3.0.5-SNAPSHOT")); + + assertThat(forA.getVersion()).isEqualTo("2.1.0"); + assertThat(forB.getVersion()).isEqualTo("3.0.5"); + } + + @Test + void fallsBackWhenAmbiguousCandidatesMapToDifferentTargets() throws Exception { + // Two candidates share current version but map to different releases — genuinely ambiguous. + writeVersions(""" + module-a=9.9.9 + module-b=8.8.8 + """); + MavenProject root = project("root", "1.0.0", reactorRoot); + MavenProject moduleA = project("module-a", "1.0.0-SNAPSHOT", reactorRoot.resolve("module-a")); + MavenProject moduleB = project("module-b", "1.0.0-SNAPSHOT", reactorRoot.resolve("module-b")); + + var result = policy(root, session(List.of(root, moduleA, moduleB))) + .getReleaseVersion(request("1.0.0-SNAPSHOT")); + + assertThat(result.getVersion()).isEqualTo("1.0.0"); + } + + @Test + void identifiesModuleByVersionsValueForDevVersionPhaseAfterRelease() throws Exception { + // release:prepare's map-development-versions phase passes the RELEASE version (not the + // current SNAPSHOT) as request.version, because release-plugin uses the just-computed + // release value as the base for the next-dev calculation. The reactor pom is still at + // its pre-release SNAPSHOT, so matching by pom version fails — but VERSIONS *value* + // still identifies the module. + writeVersions(""" + module-a=2.1.0 + module-b=3.0.5 + """); + MavenProject root = project("root", "1.0.0-SNAPSHOT", reactorRoot); + MavenProject moduleA = project("module-a", "2.1.1-SNAPSHOT", reactorRoot.resolve("module-a")); + MavenProject moduleB = project("module-b", "3.0.6-SNAPSHOT", reactorRoot.resolve("module-b")); + + // Simulate release-plugin's dev-version phase for module-a: request.version = 2.1.0 + var devA = policy(root, session(List.of(root, moduleA, moduleB))) + .getDevelopmentVersion(request("2.1.0")); + // And for module-b: request.version = 3.0.5 + var devB = policy(root, session(List.of(root, moduleA, moduleB))) + .getDevelopmentVersion(request("3.0.5")); + + assertThat(devA.getVersion()).isEqualTo("2.1.1-SNAPSHOT"); + assertThat(devB.getVersion()).isEqualTo("3.0.6-SNAPSHOT"); + } + + @Test + void devVersionFallbackYieldsSnapshotEvenWhenUnidentifiable() throws Exception { + // Unmapped module in dev-version phase: previously the fallback returned request.version + // unchanged, which is a release version and caused release-plugin's dev-version loop to + // spin forever waiting for a SNAPSHOT. The fallback must now always produce a SNAPSHOT. + writeVersions("other=9.9.9\n"); + MavenProject root = project("root", "1.0.0-SNAPSHOT", reactorRoot); + + // release-plugin passes a non-SNAPSHOT release version here. + var result = policy(root, session(List.of(root))) + .getDevelopmentVersion(request("1.0.0")); + + assertThat(result.getVersion()).isEqualTo("1.0.1-SNAPSHOT"); + } + + @Test + void resolvesWhenAmbiguousCandidatesShareTheSameTarget() throws Exception { + // BOM / fixed-group style: many modules at the same current version all bumped + // to the same release. The artifactId is ambiguous but the target isn't. + writeVersions(""" + module-a=0.5.0 + module-b=0.5.0 + module-c=0.5.0 + """); + MavenProject root = project("root", "1.0.0-SNAPSHOT", reactorRoot); + MavenProject moduleA = project("module-a", "0.3.2-SNAPSHOT", reactorRoot.resolve("module-a")); + MavenProject moduleB = project("module-b", "0.3.2-SNAPSHOT", reactorRoot.resolve("module-b")); + MavenProject moduleC = project("module-c", "0.3.2-SNAPSHOT", reactorRoot.resolve("module-c")); + + var release = policy(root, session(List.of(root, moduleA, moduleB, moduleC))) + .getReleaseVersion(request("0.3.2-SNAPSHOT")); + var dev = policy(root, session(List.of(root, moduleA, moduleB, moduleC))) + .getDevelopmentVersion(request("0.3.2-SNAPSHOT")); + + assertThat(release.getVersion()).isEqualTo("0.5.0"); + assertThat(dev.getVersion()).isEqualTo("0.5.1-SNAPSHOT"); + } + } + + @Nested + class ReactorRootLookup { + + @Test + void prefersRequestWorkingDirectoryAsReactorHint() throws Exception { + writeVersions("child=4.5.6\n"); + Path childDir = Files.createDirectories(reactorRoot.resolve("nested/deep/child")); + MavenProject root = project("root", "1.0.0", reactorRoot); + MavenProject child = project("child", "0.0.1-SNAPSHOT", childDir); + + var result = policyWithWorkingDir(root, session(List.of(root, child)), reactorRoot.toString()) + .getReleaseVersion(request("0.0.1-SNAPSHOT")); + + assertThat(result.getVersion()).isEqualTo("4.5.6"); + } + } + + private void writeVersions(String contents) throws IOException { + Path dir = Files.createDirectories(reactorRoot.resolve(".changeset")); + Files.writeString(dir.resolve("VERSIONS"), contents); + } + + private static MavenProject project(String artifactId, String version, Path basedir) { + Model model = new Model(); + model.setGroupId("test"); + model.setArtifactId(artifactId); + model.setVersion(version); + MavenProject project = new MavenProject(model); + project.setFile(basedir.resolve("pom.xml").toFile()); + return project; + } + + private static MavenSession session(List projects) { + MavenSession session = mock(MavenSession.class); + when(session.getProjects()).thenReturn(projects); + return session; + } + + private static ChangesetsVersionPolicy policy(MavenProject project, MavenSession session) { + return new ChangesetsVersionPolicy(project, session); + } + + private static VersionPolicyRequest request(String version) { + return new VersionPolicyRequest().setVersion(version); + } + + private static ChangesetsVersionPolicy policyWithWorkingDir(MavenProject project, MavenSession session, String ignored) { + return new ChangesetsVersionPolicy(project, session); + } +}