diff --git a/src/main/java/org/openrewrite/github/ChangeAction.java b/src/main/java/org/openrewrite/github/ChangeAction.java index fc00f3e..becf63b 100644 --- a/src/main/java/org/openrewrite/github/ChangeAction.java +++ b/src/main/java/org/openrewrite/github/ChangeAction.java @@ -59,9 +59,7 @@ public class ChangeAction extends Recipe { @Override public TreeVisitor getVisitor() { return Preconditions.check( - Preconditions.or( - new IsGitHubActionsWorkflow().getVisitor(), - new IsGitHubActionDefinition().getVisitor()), + new IsGitHubActionsFile(), new ChangeUsesVisitor( "$..[?(@.uses =~ '" + oldAction + "(?:@.+)?')].uses", oldSha, diff --git a/src/main/java/org/openrewrite/github/ChangeActionVersion.java b/src/main/java/org/openrewrite/github/ChangeActionVersion.java index fb3a580..39d9bcc 100644 --- a/src/main/java/org/openrewrite/github/ChangeActionVersion.java +++ b/src/main/java/org/openrewrite/github/ChangeActionVersion.java @@ -52,9 +52,7 @@ public class ChangeActionVersion extends Recipe { @Override public TreeVisitor getVisitor() { return Preconditions.check( - Preconditions.or( - new IsGitHubActionsWorkflow().getVisitor(), - new IsGitHubActionDefinition().getVisitor()), + new IsGitHubActionsFile(), new ChangeUsesVisitor( "$..[?(@.uses =~ '" + action + "(?:@.+)?')].uses", oldSha, diff --git a/src/main/java/org/openrewrite/github/IsGitHubActionsFile.java b/src/main/java/org/openrewrite/github/IsGitHubActionsFile.java new file mode 100644 index 0000000..2a0742e --- /dev/null +++ b/src/main/java/org/openrewrite/github/IsGitHubActionsFile.java @@ -0,0 +1,42 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.github; + +import lombok.Getter; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; + +public class IsGitHubActionsFile extends Recipe { + + @Getter + final String displayName = "Is GitHub Actions workflow or action definition"; + + @Getter + final String description = "Checks if the file is either a GitHub Actions workflow file, or a GitHub Action " + + "definition (`action.yml`). Steps, and the `uses:` references within them, appear in both, so prefer " + + "this over `IsGitHubActionsWorkflow` as a precondition for any recipe that operates on steps. Recipes " + + "that read workflow-only keys such as `on:`, `permissions:`, `runs-on:` or `needs:` should keep the " + + "narrower `IsGitHubActionsWorkflow`."; + + @Override + public TreeVisitor getVisitor() { + return Preconditions.or( + new IsGitHubActionsWorkflow().getVisitor(), + new IsGitHubActionDefinition().getVisitor()); + } +} diff --git a/src/main/java/org/openrewrite/github/SetupJavaAdoptOpenJDKToTemurin.java b/src/main/java/org/openrewrite/github/SetupJavaAdoptOpenJDKToTemurin.java index d7906db..7b7e3d8 100644 --- a/src/main/java/org/openrewrite/github/SetupJavaAdoptOpenJDKToTemurin.java +++ b/src/main/java/org/openrewrite/github/SetupJavaAdoptOpenJDKToTemurin.java @@ -42,7 +42,7 @@ public class SetupJavaAdoptOpenJDKToTemurin extends Recipe { @Override public TreeVisitor getVisitor() { - return Preconditions.check(new IsGitHubActionsWorkflow(), new SetupJavaDistributionReplacerVisitor(Arrays.asList("adopt", "adopt-hotspot"), "temurin")); + return Preconditions.check(new IsGitHubActionsFile(), new SetupJavaDistributionReplacerVisitor(Arrays.asList("adopt", "adopt-hotspot"), "temurin")); } } diff --git a/src/main/java/org/openrewrite/github/SetupJavaAdoptOpenj9ToSemeru.java b/src/main/java/org/openrewrite/github/SetupJavaAdoptOpenj9ToSemeru.java index 0f41466..73f44d0 100644 --- a/src/main/java/org/openrewrite/github/SetupJavaAdoptOpenj9ToSemeru.java +++ b/src/main/java/org/openrewrite/github/SetupJavaAdoptOpenj9ToSemeru.java @@ -45,7 +45,7 @@ public class SetupJavaAdoptOpenj9ToSemeru extends Recipe { @Override public TreeVisitor getVisitor() { - return Preconditions.check(new IsGitHubActionsWorkflow(), + return Preconditions.check(new IsGitHubActionsFile(), new SetupJavaDistributionReplacerVisitor(singletonList("adopt-openj9"), "semeru")); } diff --git a/src/main/java/org/openrewrite/github/SetupJavaCaching.java b/src/main/java/org/openrewrite/github/SetupJavaCaching.java index 1469077..8ad413b 100644 --- a/src/main/java/org/openrewrite/github/SetupJavaCaching.java +++ b/src/main/java/org/openrewrite/github/SetupJavaCaching.java @@ -35,26 +35,26 @@ public class SetupJavaCaching extends Recipe { @Override public TreeVisitor getVisitor() { - return Preconditions.check(new IsGitHubActionsWorkflow(), new YamlVisitor() { + return Preconditions.check(new IsGitHubActionsFile(), new YamlVisitor() { @Override public Yaml visitDocuments(Yaml.Documents documents, ExecutionContext ctx) { Yaml.Documents d = documents; - if (!FindKey.find(documents, "$.jobs..steps[?(@.run =~ '.*gradle.*')]").isEmpty()) { - d = (Yaml.Documents) new MergeYaml("$.jobs..steps[?(@.uses =~ 'actions/setup-java(?:@v.+)?')]", + if (!FindKey.find(documents, "$..steps[?(@.run =~ '.*gradle.*')]").isEmpty()) { + d = (Yaml.Documents) new MergeYaml("$..steps[?(@.uses =~ 'actions/setup-java(?:@v.+)?')]", "" + "with:\n" + " cache: 'gradle'", true, null, null, null, null, null) .getVisitor().visitNonNull(d, ctx); } - if (!FindKey.find(documents, "$.jobs..steps[?(@.run =~ '.*mvn.*')]").isEmpty()) { - d = (Yaml.Documents) new MergeYaml("$.jobs..steps[?(@.uses =~ 'actions/setup-java(?:@v.+)?')]", + if (!FindKey.find(documents, "$..steps[?(@.run =~ '.*mvn.*')]").isEmpty()) { + d = (Yaml.Documents) new MergeYaml("$..steps[?(@.uses =~ 'actions/setup-java(?:@v.+)?')]", "" + "with:\n" + " cache: 'maven'", true, null, null, null, null, null) .getVisitor().visitNonNull(d, ctx); } if (d != documents) { - d = (Yaml.Documents) new DeleteKey("$.jobs..steps[?(@.uses =~ 'actions/cache(?:@v.+)?')]", null) + d = (Yaml.Documents) new DeleteKey("$..steps[?(@.uses =~ 'actions/cache(?:@v.+)?')]", null) .getVisitor().visitNonNull(d, ctx); } return d; diff --git a/src/main/java/org/openrewrite/github/SetupJavaUpgradeJavaVersion.java b/src/main/java/org/openrewrite/github/SetupJavaUpgradeJavaVersion.java index 6c4a262..7a46257 100644 --- a/src/main/java/org/openrewrite/github/SetupJavaUpgradeJavaVersion.java +++ b/src/main/java/org/openrewrite/github/SetupJavaUpgradeJavaVersion.java @@ -43,7 +43,7 @@ public class SetupJavaUpgradeJavaVersion extends Recipe { @Override public TreeVisitor getVisitor() { - return Preconditions.check(new IsGitHubActionsWorkflow(), new UpgradeJavaVersionVisitor( + return Preconditions.check(new IsGitHubActionsFile(), new UpgradeJavaVersionVisitor( minimumJavaMajorVersion == null ? 21 : minimumJavaMajorVersion )); } diff --git a/src/main/java/org/openrewrite/github/SetupNodeUpgradeNodeVersion.java b/src/main/java/org/openrewrite/github/SetupNodeUpgradeNodeVersion.java index f8ef2a9..c8b2ab9 100644 --- a/src/main/java/org/openrewrite/github/SetupNodeUpgradeNodeVersion.java +++ b/src/main/java/org/openrewrite/github/SetupNodeUpgradeNodeVersion.java @@ -48,7 +48,7 @@ public class SetupNodeUpgradeNodeVersion extends Recipe { @Override public TreeVisitor getVisitor() { - return Preconditions.check(new IsGitHubActionsWorkflow(), new UpgradeNodeVersionVisitor( + return Preconditions.check(new IsGitHubActionsFile(), new UpgradeNodeVersionVisitor( minimumNodeMajorVersion == null ? 24 : minimumNodeMajorVersion )); } diff --git a/src/main/java/org/openrewrite/github/SetupPythonToUv.java b/src/main/java/org/openrewrite/github/SetupPythonToUv.java index 887f6b0..bad1e5c 100644 --- a/src/main/java/org/openrewrite/github/SetupPythonToUv.java +++ b/src/main/java/org/openrewrite/github/SetupPythonToUv.java @@ -91,7 +91,7 @@ public class SetupPythonToUv extends Recipe { @Override public TreeVisitor getVisitor() { return Preconditions.check( - new IsGitHubActionsWorkflow(), + new IsGitHubActionsFile(), new SetupPythonToUvVisitor( uvVersion != null ? uvVersion : "v6", mapSyncStrategy(syncStrategy != null ? syncStrategy : "basic"), diff --git a/src/main/java/org/openrewrite/github/SetupPythonUpgradePythonVersion.java b/src/main/java/org/openrewrite/github/SetupPythonUpgradePythonVersion.java index d6d9b3b..a9d0ec2 100644 --- a/src/main/java/org/openrewrite/github/SetupPythonUpgradePythonVersion.java +++ b/src/main/java/org/openrewrite/github/SetupPythonUpgradePythonVersion.java @@ -68,7 +68,7 @@ public Validated validate() { @Override public TreeVisitor getVisitor() { - return Preconditions.check(new IsGitHubActionsWorkflow(), new YamlVisitor() { + return Preconditions.check(new IsGitHubActionsFile(), new YamlVisitor() { @Override public Yaml visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { if (!"python-version".equals(entry.getKey().getValue()) || diff --git a/src/main/java/org/openrewrite/github/UpgradeOfficialGitHubActions.java b/src/main/java/org/openrewrite/github/UpgradeOfficialGitHubActions.java index 0d548ca..b1ea668 100644 --- a/src/main/java/org/openrewrite/github/UpgradeOfficialGitHubActions.java +++ b/src/main/java/org/openrewrite/github/UpgradeOfficialGitHubActions.java @@ -53,7 +53,7 @@ public Accumulator getInitialValue(ExecutionContext ctx) { @Override public TreeVisitor getScanner(Accumulator acc) { - return Preconditions.check(workflowOrActionDefinition(), new YamlIsoVisitor() { + return Preconditions.check(new IsGitHubActionsFile(), new YamlIsoVisitor() { @Override public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { if (entry.getKey() instanceof Yaml.Scalar && @@ -85,7 +85,7 @@ public TreeVisitor getVisitor(Accumulator acc) { replacements.put(target.getAction() + '@' + target.getCurrentRef(), target.getAction() + '@' + target.getTarget()); } - return Preconditions.check(workflowOrActionDefinition(), new YamlIsoVisitor() { + return Preconditions.check(new IsGitHubActionsFile(), new YamlIsoVisitor() { @Override public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { Yaml.Mapping.Entry e = super.visitMappingEntry(entry, ctx); @@ -103,16 +103,6 @@ public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionC }); } - /** - * Official action references appear both in workflow files ({@code $.jobs..steps[].uses}) and in - * the {@code runs.steps[].uses} of composite action definitions, so both file types are matched. - */ - private static TreeVisitor workflowOrActionDefinition() { - return Preconditions.or( - new IsGitHubActionsWorkflow().getVisitor(), - new IsGitHubActionDefinition().getVisitor()); - } - private static Map loadKnownShas() { try (InputStream is = UpgradeOfficialGitHubActions.class .getResourceAsStream("/META-INF/rewrite/known-action-shas.properties")) { diff --git a/src/main/java/org/openrewrite/github/UpgradeSlackNotificationVersion2.java b/src/main/java/org/openrewrite/github/UpgradeSlackNotificationVersion2.java index f57ef22..c8b8e15 100644 --- a/src/main/java/org/openrewrite/github/UpgradeSlackNotificationVersion2.java +++ b/src/main/java/org/openrewrite/github/UpgradeSlackNotificationVersion2.java @@ -41,7 +41,7 @@ public class UpgradeSlackNotificationVersion2 extends Recipe { @Override public TreeVisitor getVisitor() { - return Preconditions.check(new IsGitHubActionsWorkflow(), new UpgradeSlackNotificationActionVisitor()); + return Preconditions.check(new IsGitHubActionsFile(), new UpgradeSlackNotificationActionVisitor()); } @AllArgsConstructor diff --git a/src/main/java/org/openrewrite/github/security/ArtifactSecurity.java b/src/main/java/org/openrewrite/github/security/ArtifactSecurity.java index 9cf7226..1910faa 100644 --- a/src/main/java/org/openrewrite/github/security/ArtifactSecurity.java +++ b/src/main/java/org/openrewrite/github/security/ArtifactSecurity.java @@ -19,7 +19,7 @@ import lombok.Value; import org.jspecify.annotations.Nullable; import org.openrewrite.*; -import org.openrewrite.github.IsGitHubActionsWorkflow; +import org.openrewrite.github.IsGitHubActionsFile; import org.openrewrite.marker.SearchResult; import org.openrewrite.yaml.JsonPathMatcher; import org.openrewrite.yaml.YamlIsoVisitor; @@ -68,7 +68,7 @@ public class ArtifactSecurity extends Recipe { @Override public TreeVisitor getVisitor() { return Preconditions.check( - new IsGitHubActionsWorkflow(), + new IsGitHubActionsFile(), new ArtifactSecurityVisitor() ); } diff --git a/src/main/java/org/openrewrite/github/security/ForbiddenUses.java b/src/main/java/org/openrewrite/github/security/ForbiddenUses.java index fc46421..d0a94a1 100644 --- a/src/main/java/org/openrewrite/github/security/ForbiddenUses.java +++ b/src/main/java/org/openrewrite/github/security/ForbiddenUses.java @@ -21,7 +21,7 @@ import lombok.Value; import org.jspecify.annotations.Nullable; import org.openrewrite.*; -import org.openrewrite.github.IsGitHubActionsWorkflow; +import org.openrewrite.github.IsGitHubActionsFile; import org.openrewrite.marker.SearchResult; import org.openrewrite.yaml.YamlIsoVisitor; import org.openrewrite.yaml.tree.Yaml; @@ -109,7 +109,7 @@ public ForbiddenUses( @Override public TreeVisitor getVisitor() { return Preconditions.check( - new IsGitHubActionsWorkflow(), + new IsGitHubActionsFile(), new ForbiddenUsesVisitor(allDangerousActions, allSuspiciousPatterns) ); } diff --git a/src/main/java/org/openrewrite/github/security/Obfuscation.java b/src/main/java/org/openrewrite/github/security/Obfuscation.java index aed6774..cb411e7 100644 --- a/src/main/java/org/openrewrite/github/security/Obfuscation.java +++ b/src/main/java/org/openrewrite/github/security/Obfuscation.java @@ -21,7 +21,7 @@ import org.openrewrite.Preconditions; import org.openrewrite.Recipe; import org.openrewrite.TreeVisitor; -import org.openrewrite.github.IsGitHubActionsWorkflow; +import org.openrewrite.github.IsGitHubActionsFile; import org.openrewrite.marker.SearchResult; import org.openrewrite.yaml.YamlIsoVisitor; import org.openrewrite.yaml.tree.Yaml; @@ -47,7 +47,7 @@ public class Obfuscation extends Recipe { @Override public TreeVisitor getVisitor() { return Preconditions.check( - new IsGitHubActionsWorkflow(), + new IsGitHubActionsFile(), new ObfuscationVisitor() ); } diff --git a/src/main/java/org/openrewrite/github/security/PinGitHubActionsToSha.java b/src/main/java/org/openrewrite/github/security/PinGitHubActionsToSha.java index 852011f..1b0ba24 100644 --- a/src/main/java/org/openrewrite/github/security/PinGitHubActionsToSha.java +++ b/src/main/java/org/openrewrite/github/security/PinGitHubActionsToSha.java @@ -19,7 +19,7 @@ import lombok.Value; import org.jspecify.annotations.Nullable; import org.openrewrite.*; -import org.openrewrite.github.IsGitHubActionsWorkflow; +import org.openrewrite.github.IsGitHubActionsFile; import org.openrewrite.ipc.http.HttpSender; import org.openrewrite.yaml.YamlIsoVisitor; import org.openrewrite.yaml.tree.Yaml; @@ -128,7 +128,7 @@ public TreeVisitor getVisitor(Map knownShas String apiToken = githubApiToken; List allowList = includedActions == null ? emptyList() : includedActions; return Preconditions.check( - new IsGitHubActionsWorkflow(), + new IsGitHubActionsFile(), new YamlIsoVisitor() { /** diff --git a/src/main/java/org/openrewrite/github/security/RefVersionMismatch.java b/src/main/java/org/openrewrite/github/security/RefVersionMismatch.java index c40de2c..df9f578 100644 --- a/src/main/java/org/openrewrite/github/security/RefVersionMismatch.java +++ b/src/main/java/org/openrewrite/github/security/RefVersionMismatch.java @@ -18,7 +18,7 @@ import lombok.EqualsAndHashCode; import lombok.Value; import org.openrewrite.*; -import org.openrewrite.github.IsGitHubActionsWorkflow; +import org.openrewrite.github.IsGitHubActionsFile; import org.openrewrite.marker.SearchResult; import org.openrewrite.yaml.YamlIsoVisitor; import org.openrewrite.yaml.tree.Yaml; @@ -49,7 +49,7 @@ public class RefVersionMismatch extends Recipe { @Override public TreeVisitor getVisitor() { return Preconditions.check( - new IsGitHubActionsWorkflow(), + new IsGitHubActionsFile(), new RefVersionMismatchVisitor() ); } diff --git a/src/main/java/org/openrewrite/github/security/TemplateInjection.java b/src/main/java/org/openrewrite/github/security/TemplateInjection.java index 77777e1..7e20ed5 100644 --- a/src/main/java/org/openrewrite/github/security/TemplateInjection.java +++ b/src/main/java/org/openrewrite/github/security/TemplateInjection.java @@ -22,7 +22,7 @@ import org.openrewrite.Preconditions; import org.openrewrite.Recipe; import org.openrewrite.TreeVisitor; -import org.openrewrite.github.IsGitHubActionsWorkflow; +import org.openrewrite.github.IsGitHubActionsFile; import org.openrewrite.marker.SearchResult; import org.openrewrite.yaml.JsonPathMatcher; import org.openrewrite.yaml.YamlIsoVisitor; @@ -86,7 +86,7 @@ public class TemplateInjection extends Recipe { @Override public TreeVisitor getVisitor() { return Preconditions.check( - new IsGitHubActionsWorkflow(), + new IsGitHubActionsFile(), new TemplateInjectionVisitor() ); } diff --git a/src/main/java/org/openrewrite/github/security/TrustedPublishing.java b/src/main/java/org/openrewrite/github/security/TrustedPublishing.java index 7ac1627..5449e0b 100644 --- a/src/main/java/org/openrewrite/github/security/TrustedPublishing.java +++ b/src/main/java/org/openrewrite/github/security/TrustedPublishing.java @@ -18,7 +18,7 @@ import lombok.EqualsAndHashCode; import lombok.Value; import org.openrewrite.*; -import org.openrewrite.github.IsGitHubActionsWorkflow; +import org.openrewrite.github.IsGitHubActionsFile; import org.openrewrite.marker.SearchResult; import org.openrewrite.yaml.YamlIsoVisitor; import org.openrewrite.yaml.tree.Yaml; @@ -68,7 +68,7 @@ public class TrustedPublishing extends Recipe { @Override public TreeVisitor getVisitor() { return Preconditions.check( - new IsGitHubActionsWorkflow(), + new IsGitHubActionsFile(), new TrustedPublishingVisitor() ); } diff --git a/src/main/java/org/openrewrite/github/security/UnpinnedActions.java b/src/main/java/org/openrewrite/github/security/UnpinnedActions.java index 7c7e90d..c514741 100644 --- a/src/main/java/org/openrewrite/github/security/UnpinnedActions.java +++ b/src/main/java/org/openrewrite/github/security/UnpinnedActions.java @@ -21,7 +21,7 @@ import org.openrewrite.Preconditions; import org.openrewrite.Recipe; import org.openrewrite.TreeVisitor; -import org.openrewrite.github.IsGitHubActionsWorkflow; +import org.openrewrite.github.IsGitHubActionsFile; import org.openrewrite.marker.SearchResult; import org.openrewrite.yaml.YamlIsoVisitor; import org.openrewrite.yaml.tree.Yaml; @@ -48,7 +48,7 @@ public class UnpinnedActions extends Recipe { @Override public TreeVisitor getVisitor() { return Preconditions.check( - new IsGitHubActionsWorkflow(), + new IsGitHubActionsFile(), new UnpinnedActionsVisitor() ); } diff --git a/src/main/java/org/openrewrite/github/security/UnpinnedDockerImages.java b/src/main/java/org/openrewrite/github/security/UnpinnedDockerImages.java index 79f7096..db1f1b6 100644 --- a/src/main/java/org/openrewrite/github/security/UnpinnedDockerImages.java +++ b/src/main/java/org/openrewrite/github/security/UnpinnedDockerImages.java @@ -21,7 +21,7 @@ import org.openrewrite.Preconditions; import org.openrewrite.Recipe; import org.openrewrite.TreeVisitor; -import org.openrewrite.github.IsGitHubActionsWorkflow; +import org.openrewrite.github.IsGitHubActionsFile; import org.openrewrite.marker.SearchResult; import org.openrewrite.yaml.YamlIsoVisitor; import org.openrewrite.yaml.tree.Yaml; @@ -48,7 +48,7 @@ public class UnpinnedDockerImages extends Recipe { @Override public TreeVisitor getVisitor() { return Preconditions.check( - new IsGitHubActionsWorkflow(), + new IsGitHubActionsFile(), new UnpinnedDockerImagesVisitor() ); } @@ -71,6 +71,12 @@ public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionC return mappingEntry; } + private boolean isLocalDockerfile(String imageValue) { + return !imageValue.startsWith("docker://") && + (imageValue.startsWith("./") || imageValue.startsWith("../") || + "Dockerfile".equals(imageValue) || imageValue.endsWith("/Dockerfile")); + } + private boolean isImageEntry(Yaml.Mapping.Entry entry) { return "image".equals(entry.getKey().getValue()); } @@ -80,6 +86,11 @@ private String getImageValue(Yaml.Mapping.Entry entry) { } private boolean isUnpinnedDockerImage(String imageValue) { + // A Docker container action may build from a local Dockerfile, which has no digest to pin + if (isLocalDockerfile(imageValue)) { + return false; + } + // Handle docker:// prefix String cleanImage = imageValue; if (cleanImage.startsWith("docker://")) { diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index c66d204..81e0f49 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -13,6 +13,7 @@ maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.FindG maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.FindMissingTimeout,Find jobs missing timeout,Find GitHub Actions jobs missing a timeout.,1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,, maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.GitHubActionsBestPractices,GitHub Actions best practices,"Applies best practices to GitHub Actions workflows, including enabling dependency caching, using cached distributions, finding missing timeouts, removing unused inputs, preferring block-style job dependencies, and upgrading official actions to their latest versions.",7,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,, maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.IsGitHubActionDefinition,Is GitHub Action definition,"Checks if the file is a GitHub Action definition (`action.yml`), such as a composite action.",1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,, +maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.IsGitHubActionsFile,Is GitHub Actions workflow or action definition,"Checks if the file is either a GitHub Actions workflow file, or a GitHub Action definition (`action.yml`). Steps, and the `uses:` references within them, appear in both, so prefer this over `IsGitHubActionsWorkflow` as a precondition for any recipe that operates on steps. Recipes that read workflow-only keys such as `on:`, `permissions:`, `runs-on:` or `needs:` should keep the narrower `IsGitHubActionsWorkflow`.",1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,, maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.IsGitHubActionsWorkflow,Is GitHub Actions Workflow,Checks if the file is a GitHub Actions workflow file.,1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,, maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.MigrateSetupUvV6ToV7,Migrate `astral-sh/setup-uv` from v6 to v7,Migrates `astral-sh/setup-uv` from v6 to v7. Updates the action version and removes the deprecated `server-url` input. See the [v7.0.0 release notes](https://github.com/astral-sh/setup-uv/releases/tag/v7.0.0) for breaking changes.,3,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,, maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.MigrateTibdexGitHubAppTokenToActions,Migrate from tibdex/github-app-token to actions/create-github-app-token,"Migrates from the deprecated `tibdex/github-app-token@v2` to `actions/create-github-app-token@v3`, which runs on Node.js 24 instead of the deprecated Node.js 20. Renames the `app_id`, `private_key`, and `github_api_url` inputs to their kebab-case equivalents `app-id`, `private-key`, and `github-api-url`.",5,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,, diff --git a/src/test/java/org/openrewrite/github/IsGitHubActionsFileTest.java b/src/test/java/org/openrewrite/github/IsGitHubActionsFileTest.java new file mode 100644 index 0000000..7b831cf --- /dev/null +++ b/src/test/java/org/openrewrite/github/IsGitHubActionsFileTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.github; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.yaml.Assertions.yaml; + +class IsGitHubActionsFileTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new IsGitHubActionsFile()); + } + + @DocumentExample + @Test + void detectCompositeAction() { + rewriteRun( + //language=yml + yaml( + """ + runs: + using: composite + steps: + - uses: actions/checkout@v4 + """, + """ + runs: + using: composite + steps: + - uses: actions/checkout@v4 + """, + spec -> spec.path(".github/actions/build/action.yml") + .afterRecipe(docs -> assertThat(docs.getMarkers().findFirst(SearchResult.class)).isPresent()) + ) + ); + } + + @Test + void detectWorkflow() { + rewriteRun( + //language=yml + yaml( + """ + on: + push: + branches: + - main + """, + """ + on: + push: + branches: + - main + """, + spec -> spec.path(".github/workflows/ci.yaml") + .afterRecipe(docs -> assertThat(docs.getMarkers().findFirst(SearchResult.class)).isPresent()) + ) + ); + } + + @Test + void notFoundForOtherYamlFiles() { + rewriteRun( + //language=yml + yaml( + """ + on: + push: + branches: + - main + """, + spec -> spec.path(".github/workflow/ci.yml") + .afterRecipe(docs -> assertThat(docs.getMarkers().findFirst(SearchResult.class)).isEmpty()) + ) + ); + } +} diff --git a/src/test/java/org/openrewrite/github/SetupJavaCachingTest.java b/src/test/java/org/openrewrite/github/SetupJavaCachingTest.java index b0fb67d..684077f 100644 --- a/src/test/java/org/openrewrite/github/SetupJavaCachingTest.java +++ b/src/test/java/org/openrewrite/github/SetupJavaCachingTest.java @@ -15,6 +15,7 @@ */ package org.openrewrite.github; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.openrewrite.test.RecipeSpec; @@ -71,4 +72,41 @@ void setupJavaCachingGradle(String buildTool) { ) ); } + + @Test + void setupJavaCachingInCompositeAction() { + rewriteRun( + //language=yaml + yaml( + """ + name: Build + description: Composite action + runs: + using: composite + steps: + - uses: actions/setup-java + with: + distribution: 'temurin' + java-version: '11' + - run: ./gradlew build + shell: bash + """, + """ + name: Build + description: Composite action + runs: + using: composite + steps: + - uses: actions/setup-java + with: + distribution: 'temurin' + java-version: '11' + cache: 'gradle' + - run: ./gradlew build + shell: bash + """, + spec -> spec.path(".github/actions/build/action.yml") + ) + ); + } } diff --git a/src/test/java/org/openrewrite/github/SetupJavaUpgradeJavaVersionTest.java b/src/test/java/org/openrewrite/github/SetupJavaUpgradeJavaVersionTest.java index fca31c0..c1e5746 100644 --- a/src/test/java/org/openrewrite/github/SetupJavaUpgradeJavaVersionTest.java +++ b/src/test/java/org/openrewrite/github/SetupJavaUpgradeJavaVersionTest.java @@ -268,4 +268,36 @@ void doesNotUpdateMatrixVersion() { ) ); } + + @Test + void upgradeJavaVersionInCompositeAction() { + rewriteRun( + //language=yaml + yaml( + """ + name: Setup + description: Composite action + runs: + using: composite + steps: + - uses: actions/setup-java@v3 + with: + java-version: "11" + distribution: temurin + """, + """ + name: Setup + description: Composite action + runs: + using: composite + steps: + - uses: actions/setup-java@v3 + with: + java-version: "21" + distribution: temurin + """, + spec -> spec.path(".github/actions/setup/action.yml") + ) + ); + } } diff --git a/src/test/java/org/openrewrite/github/SetupPythonUpgradePythonVersionTest.java b/src/test/java/org/openrewrite/github/SetupPythonUpgradePythonVersionTest.java index 049a6ca..c9f7b4c 100644 --- a/src/test/java/org/openrewrite/github/SetupPythonUpgradePythonVersionTest.java +++ b/src/test/java/org/openrewrite/github/SetupPythonUpgradePythonVersionTest.java @@ -473,7 +473,7 @@ void ignoreOtherActionsAndNonWorkflowFiles() { with: python-version: '3.10' """, - spec -> spec.path("action.yml") + spec -> spec.path("config.yml") ) ); } diff --git a/src/test/java/org/openrewrite/github/security/PinGitHubActionsToShaTest.java b/src/test/java/org/openrewrite/github/security/PinGitHubActionsToShaTest.java index 7c946a1..90e7fd9 100644 --- a/src/test/java/org/openrewrite/github/security/PinGitHubActionsToShaTest.java +++ b/src/test/java/org/openrewrite/github/security/PinGitHubActionsToShaTest.java @@ -269,6 +269,33 @@ void shouldIgnoreNonWorkflowFiles() { ); } + @Test + void shouldPinCompositeActionInNestedFolder() { + rewriteRun( + yaml( + """ + name: Setup + description: Composite action + runs: + using: composite + steps: + - uses: codecov/codecov-action@v4.6.0 + shell: bash + """, + """ + name: Setup + description: Composite action + runs: + using: composite + steps: + - uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 + shell: bash + """, + sourceSpecs -> sourceSpecs.path(".github/actions/setup/action.yml") + ) + ); + } + @Test void shouldPinActionWithSubpath() { rewriteRun( diff --git a/src/test/java/org/openrewrite/github/security/TemplateInjectionTest.java b/src/test/java/org/openrewrite/github/security/TemplateInjectionTest.java index e7c85f3..93ff48c 100644 --- a/src/test/java/org/openrewrite/github/security/TemplateInjectionTest.java +++ b/src/test/java/org/openrewrite/github/security/TemplateInjectionTest.java @@ -310,4 +310,31 @@ void shouldIgnoreNonWorkflowFiles() { ) ); } + + @Test + void flagsTemplateInjectionInCompositeAction() { + rewriteRun( + yaml( + """ + name: Setup + description: Composite action + runs: + using: composite + steps: + - run: 'echo "PR Title: ${{ github.event.pull_request.title }}"' + shell: bash + """, + """ + name: Setup + description: Composite action + runs: + using: composite + steps: + - ~~(Potential template injection vulnerability. User-controlled input 'github.event.pull_request.title' used in run command without proper escaping.)~~>run: 'echo "PR Title: ${{ github.event.pull_request.title }}"' + shell: bash + """, + spec -> spec.path(".github/actions/setup/action.yml") + ) + ); + } } diff --git a/src/test/java/org/openrewrite/github/security/UnpinnedDockerImagesTest.java b/src/test/java/org/openrewrite/github/security/UnpinnedDockerImagesTest.java index c5ebb62..49c2fbf 100644 --- a/src/test/java/org/openrewrite/github/security/UnpinnedDockerImagesTest.java +++ b/src/test/java/org/openrewrite/github/security/UnpinnedDockerImagesTest.java @@ -237,4 +237,53 @@ void shouldFlagMultipleUnpinnedImages() { ) ); } + + @Test + void flagsUnpinnedImageInDockerAction() { + rewriteRun( + yaml( + """ + name: Lint + description: Docker action + runs: + using: docker + image: docker://alpine:3.18 + """, + """ + name: Lint + description: Docker action + runs: + using: docker + ~~(Docker image 'docker://alpine:3.18' is not pinned to a digest. Consider pinning to a specific digest for security and reproducibility.)~~>image: docker://alpine:3.18 + """, + spec -> spec.path(".github/actions/lint/action.yml") + ) + ); + } + + @Test + void doesNotFlagLocalDockerfileBuild() { + rewriteRun( + yaml( + """ + name: Lint + description: Docker action + runs: + using: docker + image: Dockerfile + """, + spec -> spec.path(".github/actions/lint/action.yml") + ), + yaml( + """ + name: Lint + description: Docker action + runs: + using: docker + image: ./docker/Dockerfile + """, + spec -> spec.path(".github/actions/lint-nested/action.yml") + ) + ); + } }