Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 95 additions & 19 deletions src/main/java/org/openrewrite/github/SetupJavaUpgradeJavaVersion.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -55,36 +59,108 @@ private static class UpgradeJavaVersionVisitor extends YamlVisitor<ExecutionCont

private final int minimumJavaMajorVersion;

private static final Pattern matrixReference = Pattern.compile("\\$\\{\\{\\s*matrix\\.([a-zA-Z_][a-zA-Z0-9_-]*)\\s*}}");
private static final JsonPathMatcher job = new JsonPathMatcher("$.jobs.*");

@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);
public Yaml visitMapping(Yaml.Mapping mapping, ExecutionContext ctx) {
Yaml.Mapping m = (Yaml.Mapping) super.visitMapping(mapping, ctx);
if (!job.matches(getCursor().getParentOrThrow())) {
return m;
}
Yaml.Block steps = value(m, "steps");
if (!(steps instanceof Yaml.Sequence)) {
return m;
}
Set<String> 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<Yaml.Sequence.Entry> 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<String> seen = new HashSet<>();
List<Yaml.Sequence.Entry> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -248,7 +323,7 @@ void doesNotUpdateVersionInOtherActions() {
}

@Test
void doesNotUpdateMatrixVersion() {
void upgradesMatrixVersionAndRemovesDuplicates() {
rewriteRun(
//language=yaml
yaml(
Expand All @@ -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")
)
);
Expand Down
Loading