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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,73 +15,38 @@
*/
package org.openrewrite.github;

import lombok.EqualsAndHashCode;
import lombok.Value;
import org.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.internal.StringUtils;
import org.openrewrite.yaml.JsonPathMatcher;
import org.openrewrite.yaml.YamlIsoVisitor;
import org.openrewrite.yaml.YamlParser;
import org.openrewrite.yaml.tree.Yaml;
import lombok.Getter;
import org.openrewrite.Recipe;
import org.openrewrite.yaml.MergeYaml;

@EqualsAndHashCode(callSuper = false)
@SuppressWarnings("ConcatenationWithEmptyString")
@Value
public class AutoCancelInProgressWorkflow extends Recipe {
@Option(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.",
required = false,
example = "WORKFLOWS_ACCESS_TOKEN")
@Nullable
String accessToken;

String displayName = "Cancel in-progress workflow when it is triggered again";
import java.util.List;

String description = "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.";
import static java.util.Collections.singletonList;

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
JsonPathMatcher firstStep = new JsonPathMatcher("$.jobs..steps[:1].uses");
JsonPathMatcher jobSteps = new JsonPathMatcher("$.jobs..steps.*");
public class AutoCancelInProgressWorkflow extends Recipe {

String userProvidedAccessTokenTemplate = "" +
"- uses: styfle/cancel-workflow-action@0.9.1\n" +
" with:\n" +
" access_token: ${{ secrets." + accessToken + " }}";
private static final String CONCURRENCY = "concurrency:\n" +
" group: ${{ github.workflow }}-${{ github.ref }}\n" +
" cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}";

String defaultAccessTokenTemplate = "" +
"- uses: styfle/cancel-workflow-action@0.9.1\n" +
" with:\n" +
" access_token: ${{ github.token }}";
@Getter
final String displayName = "Cancel in-progress workflow when it is triggered again";

return Preconditions.check(new IsGitHubActionsWorkflow(), new YamlIsoVisitor<ExecutionContext>() {
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
if (firstStep.matches(getCursor()) && (!(entry.getValue() instanceof Yaml.Scalar) ||
!((Yaml.Scalar) entry.getValue()).getValue().contains("cancel-workflow-action"))) {
getCursor().dropParentUntil(Yaml.Sequence.class::isInstance).putMessage("ADD_STEP", true);
}
return super.visitMappingEntry(entry, ctx);
}
@Getter
final String description = "When a workflow is already running and would be triggered again, cancel the existing workflow, " +
"through the native [`concurrency`](https://docs.github.com/en/actions/using-jobs/using-concurrency) property. " +
"Runs on the default branch are not cancelled.";

@Override
public Yaml.Sequence visitSequence(Yaml.Sequence sequence, ExecutionContext ctx) {
Yaml.Sequence s = super.visitSequence(sequence, ctx);
if (jobSteps.matches(getCursor()) && Boolean.TRUE.equals(getCursor().getMessage("ADD_STEP"))) {
Yaml.Documents documents = new YamlParser()
.parse(ctx, StringUtils.isNullOrEmpty(accessToken) ? defaultAccessTokenTemplate : userProvidedAccessTokenTemplate)
.map(Yaml.Documents.class::cast)
.findFirst()
.get();
Yaml.Sequence.Entry cancelWorkflowAction = ((Yaml.Sequence) documents.getDocuments().get(0).getBlock()).getEntries().get(0);
cancelWorkflowAction = autoFormat(cancelWorkflowAction.withPrefix("\n"), ctx, getCursor());
return s.withEntries(ListUtils.concat(cancelWorkflowAction, s.getEntries()));
}
return s;
}
});
@Override
public List<Recipe> getRecipeList() {
return singletonList(new MergeYaml(
"$",
CONCURRENCY,
true,
null,
".github/workflows/*.{yml,yaml}",
MergeYaml.InsertMode.Before,
"jobs",
null));
}
}
22 changes: 14 additions & 8 deletions src/main/resources/META-INF/rewrite/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,25 +80,31 @@ examples:
type: specs.openrewrite.org/v1beta/example
recipeName: org.openrewrite.github.AutoCancelInProgressWorkflow
examples:
- description: '`AutoCancelInProgressWorkflowTest#useDefaultAccessToken`'
parameters:
- 'null'
- description: '`AutoCancelInProgressWorkflowTest#addConcurrency`'
sources:
- before: |
on:
push:
branches:
- main
jobs:
build:
runs-on: linux
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
after: |
on:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
build:
runs-on: linux
steps:
- uses: styfle/cancel-workflow-action@0.9.1
with:
access_token: ${{ github.token }}
- uses: actions/checkout@v2
- uses: actions/checkout@v4
path: .github/workflows/ci.yml
language: yaml
---
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/META-INF/rewrite/recipes.csv
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ ecosystem,packageName,name,displayName,description,recipeCount,category1,categor
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""}]",
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.AddManualTrigger,Add manual workflow trigger,"You can manually trigger workflow runs. To trigger specific workflows in a repository, use the `workflow_dispatch` event.",2,,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.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, through the native [`concurrency`](https://docs.github.com/en/actions/using-jobs/using-concurrency) property. Runs on the default branch are not cancelled.",2,,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"":""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""},{""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.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.ChangeDependabotScheduleInterval,Change dependabot schedule interval,Change the schedule interval for a given package-ecosystem in a `dependabot.yml` configuration file. [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"":""packageEcosystem"",""type"":""String"",""displayName"":""Package ecosystem"",""description"":""The package-ecosystem to make updates on."",""example"":""maven"",""required"":true},{""name"":""interval"",""type"":""String"",""displayName"":""Schedule interval"",""description"":""The schedule interval value the package-ecosystem should use."",""example"":""weekly"",""valid"":[""daily"",""weekly"",""monthly""],""required"":true}]",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,65 +17,86 @@

import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.yaml.Assertions.yaml;

class AutoCancelInProgressWorkflowTest implements RewriteTest {

@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new AutoCancelInProgressWorkflow());
}

@DocumentExample
@Test
void useDefaultAccessToken() {
void addConcurrency() {
rewriteRun(
spec -> spec.recipe(new AutoCancelInProgressWorkflow(null)),
//language=yaml
yaml(
"""
on:
push:
branches:
- main
jobs:
build:
runs-on: linux
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
""",
"""
on:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
build:
runs-on: linux
steps:
- uses: styfle/cancel-workflow-action@0.9.1
with:
access_token: ${{ github.token }}
- uses: actions/checkout@v2
- uses: actions/checkout@v4
""",
spec -> spec.path(".github/workflows/ci.yml")
)
);
}

@Test
void useUserProvidedAccessToken() {
void retainExistingConcurrency() {
rewriteRun(
spec -> spec.recipe(new AutoCancelInProgressWorkflow("WORKFLOWS_ACCESS_TOKEN")),
//language=yaml
yaml(
"""
on: push
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
build:
runs-on: linux
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
""",
spec -> spec.path(".github/workflows/ci.yml")
)
);
}

@Test
void notAWorkflowFile() {
rewriteRun(
//language=yaml
yaml(
"""
jobs:
build:
runs-on: linux
steps:
- uses: styfle/cancel-workflow-action@0.9.1
with:
access_token: ${{ secrets.WORKFLOWS_ACCESS_TOKEN }}
- uses: actions/checkout@v2
""",
spec -> spec.path(".github/workflows/ci.yml")
spec -> spec.path("src/main/resources/application.yml")
)
);
}
Expand Down
Loading