diff --git a/src/main/java/org/openrewrite/github/SetupJavaUpgradeJavaVersion.java b/src/main/java/org/openrewrite/github/SetupJavaUpgradeJavaVersion.java index 7a46257..b816baf 100644 --- a/src/main/java/org/openrewrite/github/SetupJavaUpgradeJavaVersion.java +++ b/src/main/java/org/openrewrite/github/SetupJavaUpgradeJavaVersion.java @@ -22,8 +22,12 @@ import org.openrewrite.*; import org.openrewrite.yaml.JsonPathMatcher; import org.openrewrite.yaml.YamlVisitor; +import org.openrewrite.internal.ListUtils; import org.openrewrite.yaml.tree.Yaml; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -55,36 +59,108 @@ private static class UpgradeJavaVersionVisitor extends YamlVisitor axes = new HashSet<>(); + for (Yaml.Sequence.Entry step : ((Yaml.Sequence) steps).getEntries()) { + if (!(step.getBlock() instanceof Yaml.Mapping)) { + continue; + } + Yaml.Mapping stepMapping = (Yaml.Mapping) step.getBlock(); + Yaml.Block uses = value(stepMapping, "uses"); + Yaml.Block with = value(stepMapping, "with"); + if (!(uses instanceof Yaml.Scalar) || !((Yaml.Scalar) uses).getValue().startsWith("actions/setup-java@") || + !(with instanceof Yaml.Mapping)) { + continue; + } + Yaml.Block version = value((Yaml.Mapping) with, "java-version"); + if (version instanceof Yaml.Scalar) { + Matcher reference = matrixReference.matcher(((Yaml.Scalar) version).getValue()); + if (reference.matches()) { + axes.add(reference.group(1)); + } + } + } + return m.withEntries(ListUtils.map(m.getEntries(), strategy -> { + if (!"strategy".equals(strategy.getKey().getValue()) || !(strategy.getValue() instanceof Yaml.Mapping)) { + return strategy; + } + Yaml.Mapping strategyValue = (Yaml.Mapping) strategy.getValue(); + return strategy.withValue(strategyValue.withEntries(ListUtils.map(strategyValue.getEntries(), matrix -> { + if (!"matrix".equals(matrix.getKey().getValue()) || !(matrix.getValue() instanceof Yaml.Mapping)) { + return matrix; + } + Yaml.Mapping matrixValue = (Yaml.Mapping) matrix.getValue(); + // Includes and exclusions can encode relationships between axes. Do not invalidate them. + if (value(matrixValue, "include") != null || value(matrixValue, "exclude") != null) { + return matrix; + } + return matrix.withValue(matrixValue.withEntries(ListUtils.map(matrixValue.getEntries(), axis -> { + if (!axes.contains(axis.getKey().getValue()) || !(axis.getValue() instanceof Yaml.Sequence)) { + return axis; + } + Yaml.Sequence values = (Yaml.Sequence) axis.getValue(); + List upgraded = ListUtils.map(values.getEntries(), item -> + item.getBlock() instanceof Yaml.Scalar ? + item.withBlock(upgrade((Yaml.Scalar) item.getBlock())) : item); + if (upgraded == values.getEntries()) { + return axis; + } + Set seen = new HashSet<>(); + List distinct = ListUtils.map(upgraded, item -> + item.getBlock() instanceof Yaml.Scalar && + !seen.add(((Yaml.Scalar) item.getBlock()).getValue()) ? null : item); + if (distinct.size() != upgraded.size()) { + String trailingComma = upgraded.get(upgraded.size() - 1).getTrailingCommaPrefix(); + distinct = ListUtils.mapLast(distinct, item -> item.withTrailingCommaPrefix(trailingComma)); + } + return axis.withValue(values.withEntries(distinct)); + }))); + }))); + })); + } - Yaml.Scalar currentValue = (Yaml.Scalar) entry.getValue(); + private static Yaml.@Nullable Block value(Yaml.Mapping mapping, String key) { + return mapping.getEntries().stream().filter(e -> key.equals(e.getKey().getValue())) + .map(Yaml.Mapping.Entry::getValue).findFirst().orElse(null); + } - // specific versions are allowed by `actions/setup-java` - Matcher matcher = javaVersionPattern.matcher(currentValue.getValue()); - if (!matcher.matches()) { - return super.visitMappingEntry(entry, ctx); + private Yaml.Scalar upgrade(Yaml.Scalar scalar) { + Matcher matcher = javaVersionPattern.matcher(scalar.getValue()); + if (matcher.matches()) { + try { + if (Integer.parseInt(matcher.group(1)) < minimumJavaMajorVersion) { + return scalar.withValue(String.valueOf(minimumJavaMajorVersion)); + } + } catch (NumberFormatException ignored) { + // Leave values outside the supported integer range unchanged. + } } + return scalar; + } - int currentMajorVersion; - try { - currentMajorVersion = Integer.parseInt(matcher.group(1)); - } catch (NumberFormatException ex) { + @Override + public Yaml visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + if (!"java-version".equals(entry.getKey().getValue()) || + !javaVersion.matches(getCursor())) { return super.visitMappingEntry(entry, ctx); } - if (currentMajorVersion >= minimumJavaMajorVersion) { + if (!(entry.getValue() instanceof Yaml.Scalar)) { return super.visitMappingEntry(entry, ctx); } - - return super.visitMappingEntry( - entry.withValue(currentValue.withValue(String.valueOf(minimumJavaMajorVersion))), - ctx - ); + return super.visitMappingEntry(entry.withValue(upgrade((Yaml.Scalar) entry.getValue())), ctx); } } } diff --git a/src/test/java/org/openrewrite/github/SetupJavaUpgradeJavaVersionTest.java b/src/test/java/org/openrewrite/github/SetupJavaUpgradeJavaVersionTest.java index c1e5746..9fc5516 100644 --- a/src/test/java/org/openrewrite/github/SetupJavaUpgradeJavaVersionTest.java +++ b/src/test/java/org/openrewrite/github/SetupJavaUpgradeJavaVersionTest.java @@ -29,6 +29,81 @@ public void defaults(RecipeSpec spec) { spec.recipe(new SetupJavaUpgradeJavaVersion(21)); } + @Test + void updatesReferencedMatrixInItsOwnJob() { + rewriteRun( + yaml( + """ + jobs: + build: + strategy: + matrix: + java: [ '17', '26' ] + steps: + - uses: actions/setup-java@v4 + with: + java-version: ${{matrix.java}} + other: + strategy: + matrix: + java: [ '17' ] + steps: + - run: echo unchanged + """, + """ + jobs: + build: + strategy: + matrix: + java: [ '21', '26' ] + steps: + - uses: actions/setup-java@v4 + with: + java-version: ${{matrix.java}} + other: + strategy: + matrix: + java: [ '17' ] + steps: + - run: echo unchanged + """, + spec -> spec.path(".github/workflows/ci.yml") + ) + ); + } + + @Test + void preservesMatricesWithCrossAxisRulesAndDynamicVersions() { + rewriteRun( + yaml( + """ + jobs: + build: + strategy: + matrix: + java: [17, 21] + os: [ubuntu-latest, windows-latest] + exclude: + - java: 17 + os: windows-latest + steps: + - uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java }} + dynamic: + strategy: + matrix: + java: [17] + steps: + - uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java || matrix.java }} + """, + spec -> spec.path(".github/workflows/ci.yml") + ) + ); + } + @DocumentExample @Test void updatesOldMajorVersion() { @@ -248,7 +323,7 @@ void doesNotUpdateVersionInOtherActions() { } @Test - void doesNotUpdateMatrixVersion() { + void upgradesMatrixVersionAndRemovesDuplicates() { rewriteRun( //language=yaml yaml( @@ -264,6 +339,18 @@ void doesNotUpdateMatrixVersion() { with: java-version: ${{ matrix.java-version }} """, + """ + jobs: + build: + strategy: + matrix: + java-version: [21] + steps: + - name: set-up-jdk + uses: actions/setup-java@v2.3.0 + with: + java-version: ${{ matrix.java-version }} + """, spec -> spec.path(".github/workflows/ci.yml") ) );