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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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, ExecutionContext> 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")
+ )
+ );
+ }
}