Skip to content

Commit 14e83f0

Browse files
authored
feat(gwkt): allow using steps with logic in Kotlin (#1210)
Closes #136.
1 parent 8f77d9a commit 14e83f0

File tree

10 files changed

+280
-6
lines changed

10 files changed

+280
-6
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ confidence.
3737
* **no duplication** - don't repeat yourself! Share common configuration using constant values, or define your own
3838
functions to encapsulate logic
3939
* **fully featured language** - use the full power of Kotlin to generate workflows dynamically, randomly generate data,
40-
or add custom validation
40+
or add custom validation. Defining workflow logic in Kotlin is currently experimental
4141
* **built-in support for over 100 actions** - the most popular actions can be used in a type-safe manner thanks to the
4242
bundled bindings. For more information, see
4343
[Supported actions](https://typesafegithub.github.io/github-workflows-kt/supported-actions/)

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ confidence.
2929
* **no duplication** - don't repeat yourself! Share common configuration using constant values, or define your own
3030
functions to encapsulate logic
3131
* **fully featured language** - use the full power of Kotlin to generate workflows dynamically, randomly generate data,
32-
or add custom validation
32+
or add custom validation. Defining workflow logic in Kotlin is currently experimental
3333
* **built-in support for over 100 actions** - the most popular actions can be used in a type-safe manner thanks to the
3434
bundled bindings. For more information, see
3535
[Supported actions](supported-actions.md)

github-workflows-kt/api/github-workflows-kt.api

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
public abstract interface annotation class io/github/typesafegithub/workflows/annotations/ExperimentalClientSideBindings : java/lang/annotation/Annotation {
22
}
33

4+
public abstract interface annotation class io/github/typesafegithub/workflows/annotations/ExperimentalKotlinLogicStep : java/lang/annotation/Annotation {
5+
}
6+
47
public abstract class io/github/typesafegithub/workflows/domain/AbstractResult {
58
public final fun eq (Lio/github/typesafegithub/workflows/domain/AbstractResult$Status;)Ljava/lang/String;
69
public final fun neq (Lio/github/typesafegithub/workflows/domain/AbstractResult$Status;)Ljava/lang/String;
@@ -199,6 +202,38 @@ public final class io/github/typesafegithub/workflows/domain/JobOutputs$Ref : ko
199202
public synthetic fun setValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V
200203
}
201204

205+
public final class io/github/typesafegithub/workflows/domain/KotlinLogicStep : io/github/typesafegithub/workflows/domain/Step {
206+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function0;)V
207+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
208+
public final fun component1 ()Ljava/lang/String;
209+
public final fun component10 ()Ljava/util/Map;
210+
public final fun component11 ()Lkotlin/jvm/functions/Function0;
211+
public final fun component2 ()Ljava/lang/String;
212+
public final fun component3 ()Ljava/lang/String;
213+
public final fun component4 ()Ljava/util/LinkedHashMap;
214+
public final fun component5 ()Ljava/lang/String;
215+
public final fun component6 ()Ljava/lang/Boolean;
216+
public final fun component7 ()Ljava/lang/Integer;
217+
public final fun component8 ()Lio/github/typesafegithub/workflows/domain/Shell;
218+
public final fun component9 ()Ljava/lang/String;
219+
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function0;)Lio/github/typesafegithub/workflows/domain/KotlinLogicStep;
220+
public static synthetic fun copy$default (Lio/github/typesafegithub/workflows/domain/KotlinLogicStep;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/domain/KotlinLogicStep;
221+
public fun equals (Ljava/lang/Object;)Z
222+
public final fun getCommand ()Ljava/lang/String;
223+
public fun getCondition ()Ljava/lang/String;
224+
public fun getContinueOnError ()Ljava/lang/Boolean;
225+
public fun getEnv ()Ljava/util/LinkedHashMap;
226+
public fun getId ()Ljava/lang/String;
227+
public final fun getLogic ()Lkotlin/jvm/functions/Function0;
228+
public final fun getName ()Ljava/lang/String;
229+
public final fun getShell ()Lio/github/typesafegithub/workflows/domain/Shell;
230+
public fun getTimeoutMinutes ()Ljava/lang/Integer;
231+
public final fun getWorkingDirectory ()Ljava/lang/String;
232+
public fun get_customArguments ()Ljava/util/Map;
233+
public fun hashCode ()I
234+
public fun toString ()Ljava/lang/String;
235+
}
236+
202237
public final class io/github/typesafegithub/workflows/domain/Mode : java/lang/Enum {
203238
public static final field None Lio/github/typesafegithub/workflows/domain/Mode;
204239
public static final field Read Lio/github/typesafegithub/workflows/domain/Mode;
@@ -1739,8 +1774,8 @@ public abstract interface class io/github/typesafegithub/workflows/dsl/HasCustom
17391774
}
17401775

17411776
public final class io/github/typesafegithub/workflows/dsl/JobBuilder : io/github/typesafegithub/workflows/dsl/HasCustomArguments {
1742-
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/github/typesafegithub/workflows/domain/RunnerType;Ljava/util/List;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Concurrency;Lio/github/typesafegithub/workflows/domain/Container;Ljava/util/Map;Lio/github/typesafegithub/workflows/domain/JobOutputs;Ljava/util/Map;)V
1743-
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/github/typesafegithub/workflows/domain/RunnerType;Ljava/util/List;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Concurrency;Lio/github/typesafegithub/workflows/domain/Container;Ljava/util/Map;Lio/github/typesafegithub/workflows/domain/JobOutputs;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
1777+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/github/typesafegithub/workflows/domain/RunnerType;Ljava/util/List;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Concurrency;Lio/github/typesafegithub/workflows/domain/Container;Ljava/util/Map;Lio/github/typesafegithub/workflows/domain/JobOutputs;Ljava/util/Map;Lio/github/typesafegithub/workflows/dsl/WorkflowBuilder;)V
1778+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/github/typesafegithub/workflows/domain/RunnerType;Ljava/util/List;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Concurrency;Lio/github/typesafegithub/workflows/domain/Container;Ljava/util/Map;Lio/github/typesafegithub/workflows/domain/JobOutputs;Ljava/util/Map;Lio/github/typesafegithub/workflows/dsl/WorkflowBuilder;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
17441779
public final fun build ()Lio/github/typesafegithub/workflows/domain/Job;
17451780
public final fun getConcurrency ()Lio/github/typesafegithub/workflows/domain/Concurrency;
17461781
public final fun getCondition ()Ljava/lang/String;
@@ -1757,7 +1792,9 @@ public final class io/github/typesafegithub/workflows/dsl/JobBuilder : io/github
17571792
public final fun getTimeoutMinutes ()Ljava/lang/Integer;
17581793
public fun get_customArguments ()Ljava/util/Map;
17591794
public final fun run ([Lkotlin/Unit;Ljava/lang/String;Ljava/lang/String;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;)Lio/github/typesafegithub/workflows/domain/CommandStep;
1795+
public final fun run ([Lkotlin/Unit;Ljava/lang/String;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function0;)Lio/github/typesafegithub/workflows/domain/KotlinLogicStep;
17601796
public static synthetic fun run$default (Lio/github/typesafegithub/workflows/dsl/JobBuilder;[Lkotlin/Unit;Ljava/lang/String;Ljava/lang/String;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/domain/CommandStep;
1797+
public static synthetic fun run$default (Lio/github/typesafegithub/workflows/dsl/JobBuilder;[Lkotlin/Unit;Ljava/lang/String;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/domain/KotlinLogicStep;
17611798
public final fun uses ([Lkotlin/Unit;Lio/github/typesafegithub/workflows/domain/actions/Action;Ljava/lang/String;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/util/Map;)Lio/github/typesafegithub/workflows/domain/ActionStep;
17621799
public static synthetic fun uses$default (Lio/github/typesafegithub/workflows/dsl/JobBuilder;[Lkotlin/Unit;Lio/github/typesafegithub/workflows/domain/actions/Action;Ljava/lang/String;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/util/Map;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/domain/ActionStep;
17631800
}
@@ -3014,8 +3051,8 @@ public final class io/github/typesafegithub/workflows/yaml/Preamble$WithOriginal
30143051
public final class io/github/typesafegithub/workflows/yaml/ToYamlKt {
30153052
public static final fun toYaml (Lio/github/typesafegithub/workflows/domain/Workflow;ZLjava/nio/file/Path;Lio/github/typesafegithub/workflows/yaml/Preamble;Z)Ljava/lang/String;
30163053
public static synthetic fun toYaml$default (Lio/github/typesafegithub/workflows/domain/Workflow;ZLjava/nio/file/Path;Lio/github/typesafegithub/workflows/yaml/Preamble;ZILjava/lang/Object;)Ljava/lang/String;
3017-
public static final fun writeToFile (Lio/github/typesafegithub/workflows/domain/Workflow;ZLjava/nio/file/Path;Lio/github/typesafegithub/workflows/yaml/Preamble;Z)V
3018-
public static synthetic fun writeToFile$default (Lio/github/typesafegithub/workflows/domain/Workflow;ZLjava/nio/file/Path;Lio/github/typesafegithub/workflows/yaml/Preamble;ZILjava/lang/Object;)V
3054+
public static final fun writeToFile (Lio/github/typesafegithub/workflows/domain/Workflow;ZLjava/nio/file/Path;Lio/github/typesafegithub/workflows/yaml/Preamble;ZLkotlin/jvm/functions/Function1;)V
3055+
public static synthetic fun writeToFile$default (Lio/github/typesafegithub/workflows/domain/Workflow;ZLjava/nio/file/Path;Lio/github/typesafegithub/workflows/yaml/Preamble;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
30193056
}
30203057

30213058
public final class io/github/typesafegithub/workflows/yaml/TriggersToYamlKt {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package io.github.typesafegithub.workflows.annotations
2+
3+
@Target(AnnotationTarget.FUNCTION)
4+
@RequiresOptIn
5+
public annotation class ExperimentalKotlinLogicStep

github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/Step.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,27 @@ public data class CommandStep(
4545
_customArguments = _customArguments,
4646
)
4747

48+
public data class KotlinLogicStep(
49+
override val id: String,
50+
val name: String? = null,
51+
val command: String,
52+
override val env: LinkedHashMap<String, String> = linkedMapOf(),
53+
override val condition: String? = null,
54+
override val continueOnError: Boolean? = null,
55+
override val timeoutMinutes: Int? = null,
56+
val shell: Shell? = null,
57+
val workingDirectory: String? = null,
58+
override val _customArguments: Map<String, @Contextual Any?> = emptyMap(),
59+
val logic: () -> Unit,
60+
) : Step(
61+
id = id,
62+
condition = condition,
63+
continueOnError = continueOnError,
64+
timeoutMinutes = timeoutMinutes,
65+
env = env,
66+
_customArguments = _customArguments,
67+
)
68+
4869
@Suppress("LongParameterList")
4970
public open class ActionStep<out OUTPUTS : Outputs>(
5071
override val id: String,

github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/JobBuilder.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
package io.github.typesafegithub.workflows.dsl
22

3+
import io.github.typesafegithub.workflows.annotations.ExperimentalKotlinLogicStep
34
import io.github.typesafegithub.workflows.domain.ActionStep
45
import io.github.typesafegithub.workflows.domain.CommandStep
56
import io.github.typesafegithub.workflows.domain.Concurrency
67
import io.github.typesafegithub.workflows.domain.Container
78
import io.github.typesafegithub.workflows.domain.Job
89
import io.github.typesafegithub.workflows.domain.JobOutputs
10+
import io.github.typesafegithub.workflows.domain.KotlinLogicStep
911
import io.github.typesafegithub.workflows.domain.Mode
1012
import io.github.typesafegithub.workflows.domain.Permission
1113
import io.github.typesafegithub.workflows.domain.RunnerType
1214
import io.github.typesafegithub.workflows.domain.Shell
1315
import io.github.typesafegithub.workflows.domain.actions.Action
1416
import kotlinx.serialization.Contextual
17+
import kotlin.io.path.name
1518

1619
@Suppress("LongParameterList")
1720
@GithubActionsDsl
@@ -30,6 +33,7 @@ public class JobBuilder<OUTPUT : JobOutputs>(
3033
public val services: Map<String, Container> = emptyMap(),
3134
public val jobOutputs: OUTPUT,
3235
override val _customArguments: Map<String, @Contextual Any?>,
36+
private val workflowBuilder: WorkflowBuilder,
3337
) : HasCustomArguments {
3438
private var job =
3539
Job<OUTPUT>(
@@ -87,6 +91,55 @@ public class JobBuilder<OUTPUT : JobOutputs>(
8791
return newStep
8892
}
8993

94+
@ExperimentalKotlinLogicStep
95+
public fun run(
96+
@Suppress("UNUSED_PARAMETER")
97+
vararg pleaseUseNamedArguments: Unit,
98+
name: String? = null,
99+
env: LinkedHashMap<String, String> = linkedMapOf(),
100+
@SuppressWarnings("FunctionParameterNaming")
101+
`if`: String? = null,
102+
condition: String? = null,
103+
continueOnError: Boolean? = null,
104+
timeoutMinutes: Int? = null,
105+
shell: Shell? = null,
106+
workingDirectory: String? = null,
107+
@SuppressWarnings("FunctionParameterNaming")
108+
_customArguments: Map<String, @Contextual Any> = mapOf(),
109+
logic: () -> Unit,
110+
): KotlinLogicStep {
111+
require(!(`if` != null && condition != null)) {
112+
"Either 'if' or 'condition' have to be set, not both!"
113+
}
114+
require(job.steps.filterIsInstance<ActionStep<*>>().any { "/checkout@" in it.action.usesString }) {
115+
"Please check out the code prior to using Kotlin-based 'run' block!"
116+
}
117+
val sourceFile =
118+
workflowBuilder.workflow.sourceFile
119+
?: throw IllegalArgumentException("sourceFile needs to be set when using Kotlin-based 'run' block!")
120+
val id = "step-${job.steps.size}"
121+
122+
val newStep =
123+
KotlinLogicStep(
124+
id = id,
125+
name = name,
126+
// Because of the current architecture, it's hard to make this command work properly if the sourceFile
127+
// isn't in .github/workflows directory. It's the most common use case, though, so for now this
128+
// simplified implementation is used.
129+
command = "GHWKT_RUN_STEP='${this.id}:$id' .github/workflows/${sourceFile.name}",
130+
logic = logic,
131+
env = env,
132+
condition = `if` ?: condition,
133+
continueOnError = continueOnError,
134+
timeoutMinutes = timeoutMinutes,
135+
shell = shell,
136+
workingDirectory = workingDirectory,
137+
_customArguments = _customArguments,
138+
)
139+
job = job.copy(steps = job.steps + newStep)
140+
return newStep
141+
}
142+
90143
public fun <T : Action.Outputs> uses(
91144
@Suppress("UNUSED_PARAMETER")
92145
vararg pleaseUseNamedArguments: Unit,

github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/WorkflowBuilder.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public class WorkflowBuilder(
8080
services = services,
8181
jobOutputs = outputs,
8282
_customArguments = _customArguments,
83+
workflowBuilder = this,
8384
)
8485
jobBuilder.block()
8586
val newJob = jobBuilder.build()

github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/StepsToYaml.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.github.typesafegithub.workflows.yaml
22

33
import io.github.typesafegithub.workflows.domain.ActionStep
44
import io.github.typesafegithub.workflows.domain.CommandStep
5+
import io.github.typesafegithub.workflows.domain.KotlinLogicStep
56
import io.github.typesafegithub.workflows.domain.Shell
67
import io.github.typesafegithub.workflows.domain.Shell.Bash
78
import io.github.typesafegithub.workflows.domain.Shell.Cmd
@@ -18,6 +19,7 @@ private fun Step.toYaml() =
1819
when (this) {
1920
is ActionStep<*> -> toYaml()
2021
is CommandStep -> toYaml()
22+
is KotlinLogicStep -> toYaml()
2123
}
2224

2325
private fun ActionStep<*>.toYaml(): Map<String, Any?> =
@@ -45,6 +47,19 @@ private fun CommandStep.toYaml(): Map<String, Any?> =
4547
"if" to condition,
4648
) + _customArguments
4749

50+
private fun KotlinLogicStep.toYaml(): Map<String, Any?> =
51+
mapOfNotNullValues(
52+
"id" to id,
53+
"name" to name,
54+
"env" to env.ifEmpty { null },
55+
"continue-on-error" to continueOnError,
56+
"timeout-minutes" to timeoutMinutes,
57+
"shell" to shell?.toYaml(),
58+
"working-directory" to workingDirectory,
59+
"run" to command,
60+
"if" to condition,
61+
) + _customArguments
62+
4863
private fun Shell.toYaml() =
4964
when (this) {
5065
Bash -> "bash"

github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ToYaml.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.github.typesafegithub.workflows.yaml
22

33
import io.github.typesafegithub.workflows.actions.actions.CheckoutV4
44
import io.github.typesafegithub.workflows.domain.Job
5+
import io.github.typesafegithub.workflows.domain.KotlinLogicStep
56
import io.github.typesafegithub.workflows.domain.Mode
67
import io.github.typesafegithub.workflows.domain.Permission
78
import io.github.typesafegithub.workflows.domain.RunnerType.UbuntuLatest
@@ -38,6 +39,10 @@ public fun Workflow.toYaml(
3839
preamble: Preamble? = null,
3940
generateActionBindings: Boolean = false,
4041
): String {
42+
require(this.jobs.all { it.steps.none { it is KotlinLogicStep } }) {
43+
"toYaml() currently doesn't support steps with Kotlin-based 'run' blocks!"
44+
}
45+
4146
return generateYaml(
4247
addConsistencyCheck = addConsistencyCheck,
4348
useGitDiff = false,
@@ -66,7 +71,21 @@ public fun Workflow.writeToFile(
6671
gitRootDir: Path? = sourceFile?.absolute()?.findGitRoot(),
6772
preamble: Preamble? = null,
6873
generateActionBindings: Boolean = false,
74+
getenv: (String) -> String? = { System.getenv(it) },
6975
) {
76+
val runStepEnvVar = getenv("GHWKT_RUN_STEP")
77+
78+
if (runStepEnvVar != null) {
79+
val (jobId, stepId) = runStepEnvVar.split(":")
80+
val kotlinLogicStep =
81+
this.jobs
82+
.first { it.id == jobId }
83+
.steps
84+
.first { it.id == stepId } as KotlinLogicStep
85+
kotlinLogicStep.logic()
86+
return
87+
}
88+
7089
checkNotNull(gitRootDir) {
7190
"gitRootDir must be specified explicitly when sourceFile is null"
7291
}

0 commit comments

Comments
 (0)