From 782f4a0fdf7f404dff3a948dba01bfae6b9d29da Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 24 Aug 2026 23:27:32 +0200 Subject: [PATCH 1/2] Match composite actions in step-level GitHub Actions recipes `PinGitHubActionsToSha` and friends were gated on `IsGitHubActionsWorkflow`, so they only ever ran on `.github/workflows/*.{yml,yaml}` and silently skipped composite actions, whose steps live in `action.yml` under `runs.steps`. `IsGitHubActionDefinition` already matched those files, and `ChangeAction`, `ChangeActionVersion` and `UpgradeOfficialGitHubActions` already OR'd the two preconditions together. Extract that into `GitHubActionsPreconditions.workflowOrActionDefinition()` and route every step-level recipe through it, replacing the inline copies and the private duplicate in `UpgradeOfficialGitHubActions`. Most matchers needed no change: the `setup-*` recipes use relative `..steps[?(@.uses =~ ..)]` paths and `ArtifactSecurity`/`TemplateInjection` use `$..steps[*]`, both of which already reach `runs.steps`. `SetupJavaCaching` was the exception, anchored at `$.jobs..steps`; `$..steps` is equivalent for workflows, since nothing else there has `steps`. Recipes that read workflow-only keys keep the narrower precondition, as `action.yml` has no `on:`, `jobs:`, `runs-on:`, `permissions:` or `needs:`. Also guard `UnpinnedDockerImages` against local Dockerfile builds. A Docker container action's `runs.image` is often `Dockerfile` or `./path/Dockerfile`, which has no digest to pin and would otherwise be flagged. --- .../org/openrewrite/github/ChangeAction.java | 4 +- .../github/ChangeActionVersion.java | 4 +- .../github/GitHubActionsPreconditions.java | 39 +++++++++++++++ .../SetupJavaAdoptOpenJDKToTemurin.java | 2 +- .../github/SetupJavaAdoptOpenj9ToSemeru.java | 2 +- .../openrewrite/github/SetupJavaCaching.java | 12 ++--- .../github/SetupJavaUpgradeJavaVersion.java | 2 +- .../github/SetupNodeUpgradeNodeVersion.java | 2 +- .../openrewrite/github/SetupPythonToUv.java | 2 +- .../SetupPythonUpgradePythonVersion.java | 2 +- .../github/UpgradeOfficialGitHubActions.java | 14 +----- .../UpgradeSlackNotificationVersion2.java | 2 +- .../github/security/ArtifactSecurity.java | 4 +- .../github/security/ForbiddenUses.java | 4 +- .../github/security/Obfuscation.java | 4 +- .../security/PinGitHubActionsToSha.java | 4 +- .../github/security/RefVersionMismatch.java | 4 +- .../github/security/TemplateInjection.java | 4 +- .../github/security/TrustedPublishing.java | 4 +- .../github/security/UnpinnedActions.java | 4 +- .../github/security/UnpinnedDockerImages.java | 15 +++++- .../github/SetupJavaCachingTest.java | 38 ++++++++++++++ .../SetupJavaUpgradeJavaVersionTest.java | 32 ++++++++++++ .../SetupPythonUpgradePythonVersionTest.java | 2 +- .../security/PinGitHubActionsToShaTest.java | 27 ++++++++++ .../security/TemplateInjectionTest.java | 27 ++++++++++ .../security/UnpinnedDockerImagesTest.java | 49 +++++++++++++++++++ 27 files changed, 259 insertions(+), 50 deletions(-) create mode 100644 src/main/java/org/openrewrite/github/GitHubActionsPreconditions.java diff --git a/src/main/java/org/openrewrite/github/ChangeAction.java b/src/main/java/org/openrewrite/github/ChangeAction.java index fc00f3e..cd873dd 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()), + GitHubActionsPreconditions.workflowOrActionDefinition(), 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..75525cf 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()), + GitHubActionsPreconditions.workflowOrActionDefinition(), new ChangeUsesVisitor( "$..[?(@.uses =~ '" + action + "(?:@.+)?')].uses", oldSha, diff --git a/src/main/java/org/openrewrite/github/GitHubActionsPreconditions.java b/src/main/java/org/openrewrite/github/GitHubActionsPreconditions.java new file mode 100644 index 0000000..844bab9 --- /dev/null +++ b/src/main/java/org/openrewrite/github/GitHubActionsPreconditions.java @@ -0,0 +1,39 @@ +/* + * 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.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.TreeVisitor; + +public class GitHubActionsPreconditions { + + private GitHubActionsPreconditions() { + } + + /** + * Steps, and the {@code uses:} references within them, appear both in workflow files under + * {@code $.jobs.*.steps} and in the {@code $.runs.steps} of composite action definitions. Use this + * in favor of {@link IsGitHubActionsWorkflow} alone for any recipe that operates on steps, so that + * composite actions are covered too. Recipes that read workflow-only keys such as {@code on:}, + * {@code permissions:}, {@code runs-on:} or {@code needs:} should keep the narrower precondition. + */ + public static TreeVisitor workflowOrActionDefinition() { + 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..4c3b61e 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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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..894d55c 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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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..229a409 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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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..9e4b816 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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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..8dbd612 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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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..7cba34e 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(), + GitHubActionsPreconditions.workflowOrActionDefinition(), 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..90d1f8e 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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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..06e080f 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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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..47a65b1 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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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..854d39c 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.GitHubActionsPreconditions; 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(), + GitHubActionsPreconditions.workflowOrActionDefinition(), 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..c527ed8 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.GitHubActionsPreconditions; 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(), + GitHubActionsPreconditions.workflowOrActionDefinition(), 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..e1999b9 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.GitHubActionsPreconditions; 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(), + GitHubActionsPreconditions.workflowOrActionDefinition(), 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..5d00570 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.GitHubActionsPreconditions; 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(), + GitHubActionsPreconditions.workflowOrActionDefinition(), 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..040af45 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.GitHubActionsPreconditions; 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(), + GitHubActionsPreconditions.workflowOrActionDefinition(), 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..44c9a1a 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.GitHubActionsPreconditions; 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(), + GitHubActionsPreconditions.workflowOrActionDefinition(), 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..d3ae861 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.GitHubActionsPreconditions; 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(), + GitHubActionsPreconditions.workflowOrActionDefinition(), 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..7ce442b 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.GitHubActionsPreconditions; 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(), + GitHubActionsPreconditions.workflowOrActionDefinition(), 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..52d819d 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.GitHubActionsPreconditions; 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(), + GitHubActionsPreconditions.workflowOrActionDefinition(), 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/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") + ) + ); + } } From 4601040e961177e81cf8e9251af5e2bde27c3817 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 25 Aug 2026 15:06:38 +0200 Subject: [PATCH 2/2] Combine the two preconditions into an `IsGitHubActionsFile` recipe Replaces the `GitHubActionsPreconditions.workflowOrActionDefinition()` static helper with a named recipe that ORs `IsGitHubActionsWorkflow` and `IsGitHubActionDefinition`. A static method is only reachable from Java, so declarative YAML recipes had no way to express "workflow or action definition" as a precondition; a recipe can be referenced from both. All 21 call sites now pass it to the `Preconditions.check(Recipe, ..)` overload, which is recipe-aware, rather than handing over a bare visitor. --- .../org/openrewrite/github/ChangeAction.java | 2 +- .../github/ChangeActionVersion.java | 2 +- ...nditions.java => IsGitHubActionsFile.java} | 25 ++--- .../SetupJavaAdoptOpenJDKToTemurin.java | 2 +- .../github/SetupJavaAdoptOpenj9ToSemeru.java | 2 +- .../openrewrite/github/SetupJavaCaching.java | 2 +- .../github/SetupJavaUpgradeJavaVersion.java | 2 +- .../github/SetupNodeUpgradeNodeVersion.java | 2 +- .../openrewrite/github/SetupPythonToUv.java | 2 +- .../SetupPythonUpgradePythonVersion.java | 2 +- .../github/UpgradeOfficialGitHubActions.java | 4 +- .../UpgradeSlackNotificationVersion2.java | 2 +- .../github/security/ArtifactSecurity.java | 4 +- .../github/security/ForbiddenUses.java | 4 +- .../github/security/Obfuscation.java | 4 +- .../security/PinGitHubActionsToSha.java | 4 +- .../github/security/RefVersionMismatch.java | 4 +- .../github/security/TemplateInjection.java | 4 +- .../github/security/TrustedPublishing.java | 4 +- .../github/security/UnpinnedActions.java | 4 +- .../github/security/UnpinnedDockerImages.java | 4 +- .../resources/META-INF/rewrite/recipes.csv | 1 + .../github/IsGitHubActionsFileTest.java | 97 +++++++++++++++++++ 23 files changed, 142 insertions(+), 41 deletions(-) rename src/main/java/org/openrewrite/github/{GitHubActionsPreconditions.java => IsGitHubActionsFile.java} (54%) create mode 100644 src/test/java/org/openrewrite/github/IsGitHubActionsFileTest.java diff --git a/src/main/java/org/openrewrite/github/ChangeAction.java b/src/main/java/org/openrewrite/github/ChangeAction.java index cd873dd..becf63b 100644 --- a/src/main/java/org/openrewrite/github/ChangeAction.java +++ b/src/main/java/org/openrewrite/github/ChangeAction.java @@ -59,7 +59,7 @@ public class ChangeAction extends Recipe { @Override public TreeVisitor getVisitor() { return Preconditions.check( - GitHubActionsPreconditions.workflowOrActionDefinition(), + 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 75525cf..39d9bcc 100644 --- a/src/main/java/org/openrewrite/github/ChangeActionVersion.java +++ b/src/main/java/org/openrewrite/github/ChangeActionVersion.java @@ -52,7 +52,7 @@ public class ChangeActionVersion extends Recipe { @Override public TreeVisitor getVisitor() { return Preconditions.check( - GitHubActionsPreconditions.workflowOrActionDefinition(), + new IsGitHubActionsFile(), new ChangeUsesVisitor( "$..[?(@.uses =~ '" + action + "(?:@.+)?')].uses", oldSha, diff --git a/src/main/java/org/openrewrite/github/GitHubActionsPreconditions.java b/src/main/java/org/openrewrite/github/IsGitHubActionsFile.java similarity index 54% rename from src/main/java/org/openrewrite/github/GitHubActionsPreconditions.java rename to src/main/java/org/openrewrite/github/IsGitHubActionsFile.java index 844bab9..2a0742e 100644 --- a/src/main/java/org/openrewrite/github/GitHubActionsPreconditions.java +++ b/src/main/java/org/openrewrite/github/IsGitHubActionsFile.java @@ -15,23 +15,26 @@ */ 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 GitHubActionsPreconditions { +public class IsGitHubActionsFile extends Recipe { - private GitHubActionsPreconditions() { - } + @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`."; - /** - * Steps, and the {@code uses:} references within them, appear both in workflow files under - * {@code $.jobs.*.steps} and in the {@code $.runs.steps} of composite action definitions. Use this - * in favor of {@link IsGitHubActionsWorkflow} alone for any recipe that operates on steps, so that - * composite actions are covered too. Recipes that read workflow-only keys such as {@code on:}, - * {@code permissions:}, {@code runs-on:} or {@code needs:} should keep the narrower precondition. - */ - public static TreeVisitor workflowOrActionDefinition() { + @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 4c3b61e..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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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 894d55c..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(GitHubActionsPreconditions.workflowOrActionDefinition(), + 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 229a409..8ad413b 100644 --- a/src/main/java/org/openrewrite/github/SetupJavaCaching.java +++ b/src/main/java/org/openrewrite/github/SetupJavaCaching.java @@ -35,7 +35,7 @@ public class SetupJavaCaching extends Recipe { @Override public TreeVisitor getVisitor() { - return Preconditions.check(GitHubActionsPreconditions.workflowOrActionDefinition(), new YamlVisitor() { + return Preconditions.check(new IsGitHubActionsFile(), new YamlVisitor() { @Override public Yaml visitDocuments(Yaml.Documents documents, ExecutionContext ctx) { Yaml.Documents d = documents; diff --git a/src/main/java/org/openrewrite/github/SetupJavaUpgradeJavaVersion.java b/src/main/java/org/openrewrite/github/SetupJavaUpgradeJavaVersion.java index 9e4b816..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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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 8dbd612..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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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 7cba34e..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( - GitHubActionsPreconditions.workflowOrActionDefinition(), + 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 90d1f8e..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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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 06e080f..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(GitHubActionsPreconditions.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(GitHubActionsPreconditions.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); diff --git a/src/main/java/org/openrewrite/github/UpgradeSlackNotificationVersion2.java b/src/main/java/org/openrewrite/github/UpgradeSlackNotificationVersion2.java index 47a65b1..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(GitHubActionsPreconditions.workflowOrActionDefinition(), 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 854d39c..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.GitHubActionsPreconditions; +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( - GitHubActionsPreconditions.workflowOrActionDefinition(), + 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 c527ed8..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.GitHubActionsPreconditions; +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( - GitHubActionsPreconditions.workflowOrActionDefinition(), + 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 e1999b9..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.GitHubActionsPreconditions; +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( - GitHubActionsPreconditions.workflowOrActionDefinition(), + 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 5d00570..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.GitHubActionsPreconditions; +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( - GitHubActionsPreconditions.workflowOrActionDefinition(), + 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 040af45..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.GitHubActionsPreconditions; +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( - GitHubActionsPreconditions.workflowOrActionDefinition(), + 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 44c9a1a..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.GitHubActionsPreconditions; +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( - GitHubActionsPreconditions.workflowOrActionDefinition(), + 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 d3ae861..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.GitHubActionsPreconditions; +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( - GitHubActionsPreconditions.workflowOrActionDefinition(), + 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 7ce442b..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.GitHubActionsPreconditions; +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( - GitHubActionsPreconditions.workflowOrActionDefinition(), + 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 52d819d..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.GitHubActionsPreconditions; +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( - GitHubActionsPreconditions.workflowOrActionDefinition(), + new IsGitHubActionsFile(), new UnpinnedDockerImagesVisitor() ); } 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()) + ) + ); + } +}