Skip to content

Commit

Permalink
feat(gwkt): allow using steps with logic in Kotlin (#1210)
Browse files Browse the repository at this point in the history
Closes #136.
  • Loading branch information
krzema12 committed Jan 3, 2024
1 parent 8f77d9a commit 14e83f0
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 6 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ confidence.
* **no duplication** - don't repeat yourself! Share common configuration using constant values, or define your own
functions to encapsulate logic
* **fully featured language** - use the full power of Kotlin to generate workflows dynamically, randomly generate data,
or add custom validation
or add custom validation. Defining workflow logic in Kotlin is currently experimental
* **built-in support for over 100 actions** - the most popular actions can be used in a type-safe manner thanks to the
bundled bindings. For more information, see
[Supported actions](https://typesafegithub.github.io/github-workflows-kt/supported-actions/)
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ confidence.
* **no duplication** - don't repeat yourself! Share common configuration using constant values, or define your own
functions to encapsulate logic
* **fully featured language** - use the full power of Kotlin to generate workflows dynamically, randomly generate data,
or add custom validation
or add custom validation. Defining workflow logic in Kotlin is currently experimental
* **built-in support for over 100 actions** - the most popular actions can be used in a type-safe manner thanks to the
bundled bindings. For more information, see
[Supported actions](supported-actions.md)
Expand Down
45 changes: 41 additions & 4 deletions github-workflows-kt/api/github-workflows-kt.api
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
public abstract interface annotation class io/github/typesafegithub/workflows/annotations/ExperimentalClientSideBindings : java/lang/annotation/Annotation {
}

public abstract interface annotation class io/github/typesafegithub/workflows/annotations/ExperimentalKotlinLogicStep : java/lang/annotation/Annotation {
}

public abstract class io/github/typesafegithub/workflows/domain/AbstractResult {
public final fun eq (Lio/github/typesafegithub/workflows/domain/AbstractResult$Status;)Ljava/lang/String;
public final fun neq (Lio/github/typesafegithub/workflows/domain/AbstractResult$Status;)Ljava/lang/String;
Expand Down Expand Up @@ -199,6 +202,38 @@ public final class io/github/typesafegithub/workflows/domain/JobOutputs$Ref : ko
public synthetic fun setValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V
}

public final class io/github/typesafegithub/workflows/domain/KotlinLogicStep : io/github/typesafegithub/workflows/domain/Step {
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
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
public final fun component1 ()Ljava/lang/String;
public final fun component10 ()Ljava/util/Map;
public final fun component11 ()Lkotlin/jvm/functions/Function0;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/util/LinkedHashMap;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Ljava/lang/Boolean;
public final fun component7 ()Ljava/lang/Integer;
public final fun component8 ()Lio/github/typesafegithub/workflows/domain/Shell;
public final fun component9 ()Ljava/lang/String;
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;
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;
public fun equals (Ljava/lang/Object;)Z
public final fun getCommand ()Ljava/lang/String;
public fun getCondition ()Ljava/lang/String;
public fun getContinueOnError ()Ljava/lang/Boolean;
public fun getEnv ()Ljava/util/LinkedHashMap;
public fun getId ()Ljava/lang/String;
public final fun getLogic ()Lkotlin/jvm/functions/Function0;
public final fun getName ()Ljava/lang/String;
public final fun getShell ()Lio/github/typesafegithub/workflows/domain/Shell;
public fun getTimeoutMinutes ()Ljava/lang/Integer;
public final fun getWorkingDirectory ()Ljava/lang/String;
public fun get_customArguments ()Ljava/util/Map;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/github/typesafegithub/workflows/domain/Mode : java/lang/Enum {
public static final field None Lio/github/typesafegithub/workflows/domain/Mode;
public static final field Read Lio/github/typesafegithub/workflows/domain/Mode;
Expand Down Expand Up @@ -1739,8 +1774,8 @@ public abstract interface class io/github/typesafegithub/workflows/dsl/HasCustom
}

public final class io/github/typesafegithub/workflows/dsl/JobBuilder : io/github/typesafegithub/workflows/dsl/HasCustomArguments {
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
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
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
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
public final fun build ()Lio/github/typesafegithub/workflows/domain/Job;
public final fun getConcurrency ()Lio/github/typesafegithub/workflows/domain/Concurrency;
public final fun getCondition ()Ljava/lang/String;
Expand All @@ -1757,7 +1792,9 @@ public final class io/github/typesafegithub/workflows/dsl/JobBuilder : io/github
public final fun getTimeoutMinutes ()Ljava/lang/Integer;
public fun get_customArguments ()Ljava/util/Map;
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;
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;
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;
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;
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;
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;
}
Expand Down Expand Up @@ -3014,8 +3051,8 @@ public final class io/github/typesafegithub/workflows/yaml/Preamble$WithOriginal
public final class io/github/typesafegithub/workflows/yaml/ToYamlKt {
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;
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;
public static final fun writeToFile (Lio/github/typesafegithub/workflows/domain/Workflow;ZLjava/nio/file/Path;Lio/github/typesafegithub/workflows/yaml/Preamble;Z)V
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
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
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
}

public final class io/github/typesafegithub/workflows/yaml/TriggersToYamlKt {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.github.typesafegithub.workflows.annotations

@Target(AnnotationTarget.FUNCTION)
@RequiresOptIn
public annotation class ExperimentalKotlinLogicStep
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@ public data class CommandStep(
_customArguments = _customArguments,
)

public data class KotlinLogicStep(
override val id: String,
val name: String? = null,
val command: String,
override val env: LinkedHashMap<String, String> = linkedMapOf(),
override val condition: String? = null,
override val continueOnError: Boolean? = null,
override val timeoutMinutes: Int? = null,
val shell: Shell? = null,
val workingDirectory: String? = null,
override val _customArguments: Map<String, @Contextual Any?> = emptyMap(),
val logic: () -> Unit,
) : Step(
id = id,
condition = condition,
continueOnError = continueOnError,
timeoutMinutes = timeoutMinutes,
env = env,
_customArguments = _customArguments,
)

@Suppress("LongParameterList")
public open class ActionStep<out OUTPUTS : Outputs>(
override val id: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package io.github.typesafegithub.workflows.dsl

import io.github.typesafegithub.workflows.annotations.ExperimentalKotlinLogicStep
import io.github.typesafegithub.workflows.domain.ActionStep
import io.github.typesafegithub.workflows.domain.CommandStep
import io.github.typesafegithub.workflows.domain.Concurrency
import io.github.typesafegithub.workflows.domain.Container
import io.github.typesafegithub.workflows.domain.Job
import io.github.typesafegithub.workflows.domain.JobOutputs
import io.github.typesafegithub.workflows.domain.KotlinLogicStep
import io.github.typesafegithub.workflows.domain.Mode
import io.github.typesafegithub.workflows.domain.Permission
import io.github.typesafegithub.workflows.domain.RunnerType
import io.github.typesafegithub.workflows.domain.Shell
import io.github.typesafegithub.workflows.domain.actions.Action
import kotlinx.serialization.Contextual
import kotlin.io.path.name

@Suppress("LongParameterList")
@GithubActionsDsl
Expand All @@ -30,6 +33,7 @@ public class JobBuilder<OUTPUT : JobOutputs>(
public val services: Map<String, Container> = emptyMap(),
public val jobOutputs: OUTPUT,
override val _customArguments: Map<String, @Contextual Any?>,
private val workflowBuilder: WorkflowBuilder,
) : HasCustomArguments {
private var job =
Job<OUTPUT>(
Expand Down Expand Up @@ -87,6 +91,55 @@ public class JobBuilder<OUTPUT : JobOutputs>(
return newStep
}

@ExperimentalKotlinLogicStep
public fun run(
@Suppress("UNUSED_PARAMETER")
vararg pleaseUseNamedArguments: Unit,
name: String? = null,
env: LinkedHashMap<String, String> = linkedMapOf(),
@SuppressWarnings("FunctionParameterNaming")
`if`: String? = null,
condition: String? = null,
continueOnError: Boolean? = null,
timeoutMinutes: Int? = null,
shell: Shell? = null,
workingDirectory: String? = null,
@SuppressWarnings("FunctionParameterNaming")
_customArguments: Map<String, @Contextual Any> = mapOf(),
logic: () -> Unit,
): KotlinLogicStep {
require(!(`if` != null && condition != null)) {
"Either 'if' or 'condition' have to be set, not both!"
}
require(job.steps.filterIsInstance<ActionStep<*>>().any { "/checkout@" in it.action.usesString }) {
"Please check out the code prior to using Kotlin-based 'run' block!"
}
val sourceFile =
workflowBuilder.workflow.sourceFile
?: throw IllegalArgumentException("sourceFile needs to be set when using Kotlin-based 'run' block!")
val id = "step-${job.steps.size}"

val newStep =
KotlinLogicStep(
id = id,
name = name,
// Because of the current architecture, it's hard to make this command work properly if the sourceFile
// isn't in .github/workflows directory. It's the most common use case, though, so for now this
// simplified implementation is used.
command = "GHWKT_RUN_STEP='${this.id}:$id' .github/workflows/${sourceFile.name}",
logic = logic,
env = env,
condition = `if` ?: condition,
continueOnError = continueOnError,
timeoutMinutes = timeoutMinutes,
shell = shell,
workingDirectory = workingDirectory,
_customArguments = _customArguments,
)
job = job.copy(steps = job.steps + newStep)
return newStep
}

public fun <T : Action.Outputs> uses(
@Suppress("UNUSED_PARAMETER")
vararg pleaseUseNamedArguments: Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public class WorkflowBuilder(
services = services,
jobOutputs = outputs,
_customArguments = _customArguments,
workflowBuilder = this,
)
jobBuilder.block()
val newJob = jobBuilder.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.github.typesafegithub.workflows.yaml

import io.github.typesafegithub.workflows.domain.ActionStep
import io.github.typesafegithub.workflows.domain.CommandStep
import io.github.typesafegithub.workflows.domain.KotlinLogicStep
import io.github.typesafegithub.workflows.domain.Shell
import io.github.typesafegithub.workflows.domain.Shell.Bash
import io.github.typesafegithub.workflows.domain.Shell.Cmd
Expand All @@ -18,6 +19,7 @@ private fun Step.toYaml() =
when (this) {
is ActionStep<*> -> toYaml()
is CommandStep -> toYaml()
is KotlinLogicStep -> toYaml()
}

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

private fun KotlinLogicStep.toYaml(): Map<String, Any?> =
mapOfNotNullValues(
"id" to id,
"name" to name,
"env" to env.ifEmpty { null },
"continue-on-error" to continueOnError,
"timeout-minutes" to timeoutMinutes,
"shell" to shell?.toYaml(),
"working-directory" to workingDirectory,
"run" to command,
"if" to condition,
) + _customArguments

private fun Shell.toYaml() =
when (this) {
Bash -> "bash"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.github.typesafegithub.workflows.yaml

import io.github.typesafegithub.workflows.actions.actions.CheckoutV4
import io.github.typesafegithub.workflows.domain.Job
import io.github.typesafegithub.workflows.domain.KotlinLogicStep
import io.github.typesafegithub.workflows.domain.Mode
import io.github.typesafegithub.workflows.domain.Permission
import io.github.typesafegithub.workflows.domain.RunnerType.UbuntuLatest
Expand Down Expand Up @@ -38,6 +39,10 @@ public fun Workflow.toYaml(
preamble: Preamble? = null,
generateActionBindings: Boolean = false,
): String {
require(this.jobs.all { it.steps.none { it is KotlinLogicStep } }) {
"toYaml() currently doesn't support steps with Kotlin-based 'run' blocks!"
}

return generateYaml(
addConsistencyCheck = addConsistencyCheck,
useGitDiff = false,
Expand Down Expand Up @@ -66,7 +71,21 @@ public fun Workflow.writeToFile(
gitRootDir: Path? = sourceFile?.absolute()?.findGitRoot(),
preamble: Preamble? = null,
generateActionBindings: Boolean = false,
getenv: (String) -> String? = { System.getenv(it) },
) {
val runStepEnvVar = getenv("GHWKT_RUN_STEP")

if (runStepEnvVar != null) {
val (jobId, stepId) = runStepEnvVar.split(":")
val kotlinLogicStep =
this.jobs
.first { it.id == jobId }
.steps
.first { it.id == stepId } as KotlinLogicStep
kotlinLogicStep.logic()
return
}

checkNotNull(gitRootDir) {
"gitRootDir must be specified explicitly when sourceFile is null"
}
Expand Down

0 comments on commit 14e83f0

Please sign in to comment.