diff --git a/src/main/java/org/openrewrite/github/ChangeAction.java b/src/main/java/org/openrewrite/github/ChangeAction.java index f04e8c2..46f1699 100644 --- a/src/main/java/org/openrewrite/github/ChangeAction.java +++ b/src/main/java/org/openrewrite/github/ChangeAction.java @@ -17,8 +17,8 @@ import lombok.EqualsAndHashCode; import lombok.Value; +import org.jspecify.annotations.Nullable; import org.openrewrite.*; -import org.openrewrite.yaml.ChangeValue; @EqualsAndHashCode(callSuper = false) @Value @@ -38,6 +38,18 @@ public class ChangeAction extends Recipe { example = "v3") String newVersion; + @Option(displayName = "Old commit SHA", + description = "Restricts the change by the existing `uses:` ref. When omitted, the " + + "action is changed regardless of how it is pinned (the default; commit SHA pins " + + "are rewritten). When set to an empty string, only references that are **not** " + + "pinned to a 40-character commit SHA are changed, leaving deliberate SHA pins on " + + "the original action untouched. When set to a specific commit SHA, only " + + "references pinned to exactly that SHA are changed.", + required = false, + example = "8f4b7f84864484a7bf31766abe9204da3cbe65b3") + @Nullable + String oldSha; + String displayName = "Change GitHub Action"; String description = "Change a GitHub Action in any workflow."; @@ -46,8 +58,9 @@ public class ChangeAction extends Recipe { public TreeVisitor getVisitor() { return Preconditions.check( new IsGitHubActionsWorkflow(), - new ChangeValue( + new ChangeUsesVisitor( "$.jobs..[?(@.uses =~ '" + oldAction + "(?:@.+)?')].uses", - newAction + '@' + newVersion, null).getVisitor()); + oldSha, + current -> newAction + '@' + newVersion)); } } diff --git a/src/main/java/org/openrewrite/github/ChangeActionVersion.java b/src/main/java/org/openrewrite/github/ChangeActionVersion.java index 1d2ec97..69bfa23 100644 --- a/src/main/java/org/openrewrite/github/ChangeActionVersion.java +++ b/src/main/java/org/openrewrite/github/ChangeActionVersion.java @@ -17,11 +17,8 @@ import lombok.EqualsAndHashCode; import lombok.Value; +import org.jspecify.annotations.Nullable; import org.openrewrite.*; -import org.openrewrite.yaml.ChangeValue; -import org.openrewrite.yaml.YamlIsoVisitor; -import org.openrewrite.yaml.search.FindKey; -import org.openrewrite.yaml.tree.Yaml; @EqualsAndHashCode(callSuper = false) @Value @@ -36,6 +33,18 @@ public class ChangeActionVersion extends Recipe { example = "v4") String version; + @Option(displayName = "Old commit SHA", + description = "Restricts the change by the existing `uses:` ref. When omitted, the " + + "version is changed regardless of how the action is pinned (the default; commit " + + "SHA pins are rewritten). When set to an empty string, only references that are " + + "**not** pinned to a 40-character commit SHA are changed, preserving deliberate " + + "SHA pins. When set to a specific commit SHA, only references pinned to exactly " + + "that SHA are changed.", + required = false, + example = "8f4b7f84864484a7bf31766abe9204da3cbe65b3") + @Nullable + String oldSha; + String displayName = "Change GitHub Action version"; String description = "Change the version of a GitHub Action in any workflow."; @@ -44,30 +53,9 @@ public class ChangeActionVersion extends Recipe { public TreeVisitor getVisitor() { return Preconditions.check( new IsGitHubActionsWorkflow(), - new YamlIsoVisitor() { - @Override - public Yaml.Documents visitDocuments(Yaml.Documents documents, ExecutionContext ctx) { - Yaml.Documents docs = super.visitDocuments(documents, ctx); - // Find all 'uses' entries that match the specified action - for (Yaml uses : FindKey.find(docs, "$.jobs..[?(@.uses =~ '" + action + "(?:@.+)?')].uses")) { - if (!(uses instanceof Yaml.Mapping.Entry)) { - continue; - } - if (!(((Yaml.Mapping.Entry) uses).getValue() instanceof Yaml.Scalar)) { - continue; - } - - // Extract the old action name (without version), and replace with the new version - String oldAction = ((Yaml.Scalar) ((Yaml.Mapping.Entry) uses).getValue()).getValue().split("@")[0]; - docs = (Yaml.Documents) new ChangeValue( - "$.jobs..[?(@.uses =~ '" + oldAction + "(?:@.+)?')].uses", - oldAction + '@' + version, null) - .getVisitor() - .visitNonNull(docs, ctx); - } - return docs; - } - } - ); + new ChangeUsesVisitor( + "$.jobs..[?(@.uses =~ '" + action + "(?:@.+)?')].uses", + oldSha, + current -> current.split("@", 2)[0] + '@' + version)); } } diff --git a/src/main/java/org/openrewrite/github/ChangeUsesVisitor.java b/src/main/java/org/openrewrite/github/ChangeUsesVisitor.java new file mode 100644 index 0000000..c749e35 --- /dev/null +++ b/src/main/java/org/openrewrite/github/ChangeUsesVisitor.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025 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.jspecify.annotations.Nullable; +import org.openrewrite.ExecutionContext; +import org.openrewrite.yaml.JsonPathMatcher; +import org.openrewrite.yaml.YamlIsoVisitor; +import org.openrewrite.yaml.tree.Yaml; + +import java.util.function.UnaryOperator; + +/** + * Rewrites the value of the {@code uses:} entries selected by a JsonPath, applying a per-entry + * value transform. Entries are only rewritten when {@link UsesRefs#matchesOldSha} accepts the + * current value, so SHA pins can be preserved via the {@code oldSha} sentinel. Shared by + * {@link ChangeAction} and {@link ChangeActionVersion}; the two recipes differ only in the + * {@code rename} function they supply. + */ +class ChangeUsesVisitor extends YamlIsoVisitor { + + private final JsonPathMatcher matcher; + + @Nullable + private final String oldSha; + + private final UnaryOperator rename; + + ChangeUsesVisitor(String usesPath, @Nullable String oldSha, UnaryOperator rename) { + this.matcher = new JsonPathMatcher(usesPath); + this.oldSha = oldSha; + this.rename = rename; + } + + @Override + public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) { + Yaml.Mapping.Entry e = super.visitMappingEntry(entry, ctx); + if (!(matcher.matches(getCursor()) && e.getValue() instanceof Yaml.Scalar)) { + return e; + } + + Yaml.Scalar scalar = (Yaml.Scalar) e.getValue(); + String current = scalar.getValue(); + if (!UsesRefs.matchesOldSha(oldSha, current)) { + return e; + } + + String newValue = rename.apply(current); + if (newValue.equals(current)) { + return e; + } + + return e.withValue(scalar.withValue(newValue)); + } +} diff --git a/src/main/java/org/openrewrite/github/UsesRefs.java b/src/main/java/org/openrewrite/github/UsesRefs.java new file mode 100644 index 0000000..7732751 --- /dev/null +++ b/src/main/java/org/openrewrite/github/UsesRefs.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 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.jspecify.annotations.Nullable; + +import java.util.regex.Pattern; + +/** + * Helpers for the ref portion of a GitHub Actions {@code uses:} reference + * (e.g. the {@code v4} or commit SHA in {@code owner/repo@v4}). + */ +final class UsesRefs { + + private static final Pattern SHA_PATTERN = Pattern.compile("^[a-f0-9]{40}$"); + + private UsesRefs() { + } + + /** + * @return the ref after the first {@code @} in a {@code uses:} value, or {@code null} when the + * value has no {@code @}. + */ + static @Nullable String refOf(String usesValue) { + int at = usesValue.indexOf('@'); + return at < 0 ? null : usesValue.substring(at + 1); + } + + /** + * Decide whether a {@code uses:} value should be changed, given an {@code oldSha} sentinel that + * mirrors the Docker {@code ChangeFrom} {@code oldDigest} option: + *

+ */ + static boolean matchesOldSha(@Nullable String oldSha, String usesValue) { + String ref = refOf(usesValue); + if (oldSha == null) { + return true; + } + if (oldSha.isEmpty()) { + return ref == null || !SHA_PATTERN.matcher(ref).matches(); + } + return ref != null && ref.equals(oldSha); + } +} diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index 3933f99..84b282e 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -4,7 +4,7 @@ maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.Prefe maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.RemoveWorkflowInputArgument,Remove workflow input argument,Remove a specific input argument from calls to a reusable workflow.,1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,"[{""name"":""workflowReference"",""type"":""String"",""displayName"":""Workflow reference"",""description"":""The workflow reference to match (e.g., `org/repo/.github/workflows/myWorkflow.yml`)."",""example"":""org/repo/.github/workflows/myWorkflow.yml"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the workflow to match (e.g., `v1.2.3`)."",""example"":""v1.2.3"",""required"":true},{""name"":""inputArgumentName"",""type"":""String"",""displayName"":""Input argument name"",""description"":""The name of the input argument to remove."",""example"":""myInputToRemove"",""required"":true}]", maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.RemoveAllCronTriggers,Remove all cron triggers,Removes all cron triggers from a workflow.,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.SetupJavaAdoptOpenj9ToSemeru,Use `actions/setup-java` IBM `semeru` distribution,Adopt OpenJDK got moved to Eclipse Temurin and won't be updated anymore. It is highly recommended to migrate workflows from adopt-openj9 to IBM semeru to keep receiving software and security updates. See more details in the [Good-bye AdoptOpenJDK post](https://blog.adoptopenjdk.net/2021/08/goodbye-adoptopenjdk-hello-adoptium/).,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.ChangeAction,Change GitHub Action,Change a GitHub Action in any workflow.,1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,"[{""name"":""oldAction"",""type"":""String"",""displayName"":""Action"",""description"":""Name of the action to match."",""example"":""gradle/wrapper-validation-action"",""required"":true},{""name"":""newAction"",""type"":""String"",""displayName"":""Action"",""description"":""Name of the action to use instead."",""example"":""gradle/actions/wrapper-validation"",""required"":true},{""name"":""newVersion"",""type"":""String"",""displayName"":""Version"",""description"":""New version to use."",""example"":""v3"",""required"":true}]", +maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.ChangeAction,Change GitHub Action,Change a GitHub Action in any workflow.,1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,"[{""name"":""oldAction"",""type"":""String"",""displayName"":""Action"",""description"":""Name of the action to match."",""example"":""gradle/wrapper-validation-action"",""required"":true},{""name"":""newAction"",""type"":""String"",""displayName"":""Action"",""description"":""Name of the action to use instead."",""example"":""gradle/actions/wrapper-validation"",""required"":true},{""name"":""newVersion"",""type"":""String"",""displayName"":""Version"",""description"":""New version to use."",""example"":""v3"",""required"":true},{""name"":""oldSha"",""type"":""String"",""displayName"":""Old commit SHA"",""description"":""Restricts the change by the existing `uses:` ref. When omitted, the action is changed regardless of how it is pinned (the default; commit SHA pins are rewritten). When set to an empty string, only references that are **not** pinned to a 40-character commit SHA are changed, leaving deliberate SHA pins on the original action untouched. When set to a specific commit SHA, only references pinned to exactly that SHA are changed."",""example"":""8f4b7f84864484a7bf31766abe9204da3cbe65b3""}]", maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.ReplaceRunners,Replace runners for a job,Replaces the runners of a given job.,1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,"[{""name"":""jobName"",""type"":""String"",""displayName"":""Job Name"",""description"":""The name of the job to update, use * to affect all the workflow jobs"",""example"":""build"",""required"":true},{""name"":""runners"",""type"":""List"",""displayName"":""Runners"",""description"":""The new list of runners to set"",""example"":""ubuntu-latest"",""required"":true}]", maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.ReplaceSecrets,Replace GitHub Action secret names,Replace references to GitHub Action secrets in workflow files.,1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,"[{""name"":""oldSecretName"",""type"":""String"",""displayName"":""Old secret name"",""description"":""The name of the secret to be replaced"",""example"":""OSSRH_S01_USERNAME"",""required"":true},{""name"":""newSecretName"",""type"":""String"",""displayName"":""New secret name"",""description"":""The new secret name to use"",""example"":""SONATYPE_USERNAME"",""required"":true},{""name"":""fileMatcher"",""type"":""String"",""displayName"":""File matcher"",""description"":""Optional file path matcher"",""example"":"".github/workflows/*.{yml,yaml}""}]", maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.AddCronTrigger,Add cron workflow trigger,The `schedule` [event](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events) allows you to trigger a workflow at a scheduled time.,1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,"[{""name"":""cron"",""type"":""String"",""displayName"":""Cron expression"",""description"":""Using the [POSIX cron syntax](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07) or the non standard options @hourly @daily @weekly @weekdays @weekends @monthly @yearly."",""example"":""@daily"",""required"":true},{""name"":""workflowFileMatcher"",""type"":""String"",""displayName"":""Workflow files to match"",""description"":""Matches one or more workflows to update. Defaults to `*.{yml,yaml}`"",""example"":""build.yml""}]", @@ -39,7 +39,7 @@ maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.Setup 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.AutoCancelInProgressWorkflow,Cancel in-progress workflow when it is triggered again,"When a workflow is already running and would be triggered again, cancel the existing workflow. See [`styfle/cancel-workflow-action`](https://github.com/styfle/cancel-workflow-action) for details.",1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,"[{""name"":""accessToken"",""type"":""String"",""displayName"":""Optional access token"",""description"":""Optionally provide the key name of a repository or organization secret that contains a GitHub personal access token with permission to cancel workflows."",""example"":""WORKFLOWS_ACCESS_TOKEN""}]", maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.AddDependabotCooldown,Add cooldown periods to Dependabot configuration,"Adds a `cooldown` section to each update configuration in Dependabot files. Supports `default-days`, `semver-major-days`, `semver-minor-days`, `semver-patch-days`, `include`, and `exclude` options. This implements a security best practice where dependencies are not immediately adopted upon release, allowing time for security vendors to identify potential supply chain compromises. Cooldown applies only to version updates, not security updates. [Read more about dependency cooldowns](https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns). [The available configuration options for dependabot are listed on GitHub](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates).",1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,"[{""name"":""cooldownDays"",""type"":""Integer"",""displayName"":""Default cooldown days"",""description"":""The number of days to wait before considering a published dependency suitable for use (1-90). This delay allows security vendors time to identify potential compromises. Applied to all version types unless specific semver options are set."",""example"":""7""},{""name"":""semverMajorDays"",""type"":""Integer"",""displayName"":""Semver major cooldown days"",""description"":""The number of days to wait for major version updates (1-90). Only applies to package managers that support semantic versioning."",""example"":""14""},{""name"":""semverMinorDays"",""type"":""Integer"",""displayName"":""Semver minor cooldown days"",""description"":""The number of days to wait for minor version updates (1-90). Only applies to package managers that support semantic versioning."",""example"":""7""},{""name"":""semverPatchDays"",""type"":""Integer"",""displayName"":""Semver patch cooldown days"",""description"":""The number of days to wait for patch version updates (1-90). Only applies to package managers that support semantic versioning."",""example"":""3""},{""name"":""include"",""type"":""List"",""displayName"":""Include dependencies"",""description"":""List of up to 150 dependencies to apply cooldown to. Supports wildcard patterns with `*`. If not specified, cooldown applies to all dependencies."",""example"":""lodash, react*""},{""name"":""exclude"",""type"":""List"",""displayName"":""Exclude dependencies"",""description"":""List of up to 150 dependencies to exempt from cooldown. Supports wildcard patterns with `*`. Exclude list takes precedence over include list."",""example"":""critical-security-package""},{""name"":""excludeEcosystems"",""type"":""List"",""displayName"":""Exclude ecosystems"",""description"":""List of ecosystems to be excluded"",""example"":""github-actions""}]", -maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.ChangeActionVersion,Change GitHub Action version,Change the version of a GitHub Action in any workflow.,1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,"[{""name"":""action"",""type"":""String"",""displayName"":""Action"",""description"":""Name of the action to update."",""example"":""actions/setup-java"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""Version to use."",""example"":""v4"",""required"":true}]", +maven,org.openrewrite.recipe:rewrite-github-actions,org.openrewrite.github.ChangeActionVersion,Change GitHub Action version,Change the version of a GitHub Action in any workflow.,1,,GitHub Actions,,Recipes to perform [GitHub Actions](https://docs.github.com/en/actions) hygiene and migration tasks.,"[{""name"":""action"",""type"":""String"",""displayName"":""Action"",""description"":""Name of the action to update."",""example"":""actions/setup-java"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""Version to use."",""example"":""v4"",""required"":true},{""name"":""oldSha"",""type"":""String"",""displayName"":""Old commit SHA"",""description"":""Restricts the change by the existing `uses:` ref. When omitted, the version is changed regardless of how the action is pinned (the default; commit SHA pins are rewritten). When set to an empty string, only references that are **not** pinned to a 40-character commit SHA are changed, preserving deliberate SHA pins. When set to a specific commit SHA, only references pinned to exactly that SHA are changed."",""example"":""8f4b7f84864484a7bf31766abe9204da3cbe65b3""}]", 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.SetupJavaCaching,Setup Java dependency caching,GitHub actions supports dependency caching on Maven and Gradle projects. See the [blog post](https://github.blog/changelog/2021-08-30-github-actions-setup-java-now-supports-dependency-caching/).,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, and preferring block-style job dependencies.",6,,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/ChangeActionTest.java b/src/test/java/org/openrewrite/github/ChangeActionTest.java index 0745140..03ee24d 100644 --- a/src/test/java/org/openrewrite/github/ChangeActionTest.java +++ b/src/test/java/org/openrewrite/github/ChangeActionTest.java @@ -29,7 +29,8 @@ void changeActionInSteps() { spec -> spec.recipe(new ChangeAction( "gradle/wrapper-validation-action", "gradle/actions/wrapper-validation", - "v3")), + "v3", + null)), //language=yaml yaml( """ @@ -63,7 +64,8 @@ void changeActionInJob() { spec -> spec.recipe(new ChangeAction( "gradle/wrapper-validation-action", "gradle/actions/wrapper-validation", - "main")), + "main", + null)), //language=yaml yaml( """ @@ -111,4 +113,148 @@ void setupGradleYamlRecipe() { ) ); } + + @Test + void emptyOldShaLeavesShaPinnedActionUntouched() { + rewriteRun( + spec -> spec.recipe(new ChangeAction( + "gradle/wrapper-validation-action", + "gradle/actions/wrapper-validation", + "v3", + "")), + //language=yaml + yaml( + """ + jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: gradle/wrapper-validation-action@8ade135a41bc03ea155e62e844d188df1ea18608 + """, + source -> source.path(".github/workflows/ci.yml") + ) + ); + } + + @Test + void emptyOldShaStillRenamesTagPinnedAction() { + rewriteRun( + spec -> spec.recipe(new ChangeAction( + "gradle/wrapper-validation-action", + "gradle/actions/wrapper-validation", + "v3", + "")), + //language=yaml + yaml( + """ + jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: gradle/wrapper-validation-action@v2 + """, + """ + jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: gradle/actions/wrapper-validation@v3 + """, + source -> source.path(".github/workflows/ci.yml") + ) + ); + } + + @Test + void emptyOldShaIsPerEntry() { + rewriteRun( + spec -> spec.recipe(new ChangeAction( + "gradle/wrapper-validation-action", + "gradle/actions/wrapper-validation", + "v3", + "")), + //language=yaml + yaml( + """ + jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: gradle/wrapper-validation-action@8ade135a41bc03ea155e62e844d188df1ea18608 + - uses: gradle/wrapper-validation-action@v2 + """, + """ + jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: gradle/wrapper-validation-action@8ade135a41bc03ea155e62e844d188df1ea18608 + - uses: gradle/actions/wrapper-validation@v3 + """, + source -> source.path(".github/workflows/ci.yml") + ) + ); + } + + @Test + void specificOldShaChangesOnlyMatchingPin() { + rewriteRun( + spec -> spec.recipe(new ChangeAction( + "gradle/wrapper-validation-action", + "gradle/actions/wrapper-validation", + "v3", + "8ade135a41bc03ea155e62e844d188df1ea18608")), + //language=yaml + yaml( + """ + jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: gradle/wrapper-validation-action@8ade135a41bc03ea155e62e844d188df1ea18608 + - uses: gradle/wrapper-validation-action@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: gradle/wrapper-validation-action@v2 + """, + """ + jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: gradle/actions/wrapper-validation@v3 + - uses: gradle/wrapper-validation-action@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: gradle/wrapper-validation-action@v2 + """, + source -> source.path(".github/workflows/ci.yml") + ) + ); + } + + @Test + void nullOldShaStillRewritesShaPinnedAction() { + rewriteRun( + spec -> spec.recipe(new ChangeAction( + "gradle/wrapper-validation-action", + "gradle/actions/wrapper-validation", + "v3", + null)), + //language=yaml + yaml( + """ + jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: gradle/wrapper-validation-action@8ade135a41bc03ea155e62e844d188df1ea18608 + """, + """ + jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: gradle/actions/wrapper-validation@v3 + """, + source -> source.path(".github/workflows/ci.yml") + ) + ); + } } diff --git a/src/test/java/org/openrewrite/github/ChangeActionVersionTest.java b/src/test/java/org/openrewrite/github/ChangeActionVersionTest.java index 3fa185d..9f97523 100644 --- a/src/test/java/org/openrewrite/github/ChangeActionVersionTest.java +++ b/src/test/java/org/openrewrite/github/ChangeActionVersionTest.java @@ -27,7 +27,7 @@ class ChangeActionVersionTest implements RewriteTest { @Test void updateActionVersion() { rewriteRun( - spec -> spec.recipe(new ChangeActionVersion("actions/setup-java", "v4")), + spec -> spec.recipe(new ChangeActionVersion("actions/setup-java", "v4", null)), //language=yaml yaml( """ @@ -91,7 +91,7 @@ void updateActionVersionYaml() { @Test void updateActionVersionWithWildcard() { rewriteRun( - spec -> spec.recipe(new ChangeActionVersion("actions/nested.*", "v4")), + spec -> spec.recipe(new ChangeActionVersion("actions/nested.*", "v4", null)), //language=yaml yaml( """ @@ -116,4 +116,147 @@ void updateActionVersionWithWildcard() { ) ); } + + @Test + void emptyOldShaPreservesShaPin() { + rewriteRun( + spec -> spec.recipe(new ChangeActionVersion("actions/setup-java", "v4", "")), + //language=yaml + yaml( + """ + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-java@8ade135a41bc03ea155e62e844d188df1ea18608 + """, + source -> source.path(".github/workflows/ci.yaml") + ) + ); + } + + @Test + void emptyOldShaPreservesShaPinAndComment() { + rewriteRun( + spec -> spec.recipe(new ChangeActionVersion("actions/setup-java", "v4", "")), + //language=yaml + yaml( + """ + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-java@8ade135a41bc03ea155e62e844d188df1ea18608 # v3 + """, + source -> source.path(".github/workflows/ci.yaml") + ) + ); + } + + @Test + void emptyOldShaStillUpgradesTag() { + rewriteRun( + spec -> spec.recipe(new ChangeActionVersion("actions/setup-java", "v4", "")), + //language=yaml + yaml( + """ + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-java@v3 + """, + """ + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-java@v4 + """, + source -> source.path(".github/workflows/ci.yaml") + ) + ); + } + + @Test + void emptyOldShaIsPerEntry() { + rewriteRun( + spec -> spec.recipe(new ChangeActionVersion("actions/setup-java", "v4", "")), + //language=yaml + yaml( + """ + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-java@8ade135a41bc03ea155e62e844d188df1ea18608 + - uses: actions/setup-java@v3 + """, + """ + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-java@8ade135a41bc03ea155e62e844d188df1ea18608 + - uses: actions/setup-java@v4 + """, + source -> source.path(".github/workflows/ci.yaml") + ) + ); + } + + @Test + void specificOldShaChangesOnlyMatchingPin() { + rewriteRun( + spec -> spec.recipe(new ChangeActionVersion("actions/setup-java", "v4", + "8ade135a41bc03ea155e62e844d188df1ea18608")), + //language=yaml + yaml( + """ + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-java@8ade135a41bc03ea155e62e844d188df1ea18608 + - uses: actions/setup-java@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-java@v3 + """, + """ + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-java@v4 + - uses: actions/setup-java@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-java@v3 + """, + source -> source.path(".github/workflows/ci.yaml") + ) + ); + } + + @Test + void nullOldShaStillRewritesShaPin() { + rewriteRun( + spec -> spec.recipe(new ChangeActionVersion("actions/setup-java", "v4", null)), + //language=yaml + yaml( + """ + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-java@8ade135a41bc03ea155e62e844d188df1ea18608 + """, + """ + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-java@v4 + """, + source -> source.path(".github/workflows/ci.yaml") + ) + ); + } }