Skip to content
31 changes: 26 additions & 5 deletions github-workflows-kt/api/github-workflows-kt.api
Original file line number Diff line number Diff line change
Expand Up @@ -3237,19 +3237,40 @@ public final class io/github/typesafegithub/workflows/yaml/CaseKt {
public static final fun snakeCaseOf (Ljava/lang/String;)Ljava/lang/String;
}

public abstract interface class io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource {
}

public final class io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$BundledWithLibrary : io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource {
public static final field INSTANCE Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$BundledWithLibrary;
}

public final class io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$Given : io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource {
public fun <init> (Ljava/lang/String;)V
public final fun getVersion ()Ljava/lang/String;
}

public final class io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$InferFromClasspath : io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource {
public fun <init> ()V
public fun <init> (Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getCheckoutActionClassFQN ()Ljava/lang/String;
}

public abstract interface class io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig {
}

public final class io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig$Configuration : io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig {
public fun <init> (Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Z)V
public fun <init> (Ljava/lang/String;Ljava/util/Map;Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource;Lkotlin/jvm/functions/Function1;Z)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/util/Map;
public final fun component3 ()Lkotlin/jvm/functions/Function1;
public final fun component4 ()Z
public final fun copy (Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Z)Lio/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig$Configuration;
public static synthetic fun copy$default (Lio/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig$Configuration;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;ZILjava/lang/Object;)Lio/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig$Configuration;
public final fun component3 ()Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource;
public final fun component4 ()Lkotlin/jvm/functions/Function1;
public final fun component5 ()Z
public final fun copy (Ljava/lang/String;Ljava/util/Map;Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource;Lkotlin/jvm/functions/Function1;Z)Lio/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig$Configuration;
public static synthetic fun copy$default (Lio/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig$Configuration;Ljava/lang/String;Ljava/util/Map;Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource;Lkotlin/jvm/functions/Function1;ZILjava/lang/Object;)Lio/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig$Configuration;
public fun equals (Ljava/lang/Object;)Z
public final fun getAdditionalSteps ()Lkotlin/jvm/functions/Function1;
public final fun getCheckoutActionVersion ()Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource;
public final fun getCondition ()Ljava/lang/String;
public final fun getEnv ()Ljava/util/Map;
public final fun getUseLocalBindingsServerAsFallback ()Z
Expand Down
1 change: 1 addition & 0 deletions github-workflows-kt/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies {
implementation("it.krzeminski:snakeyaml-engine-kmp:4.0.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation(kotlin("reflect"))
implementation(projects.sharedInternal)
ksp(projects.codeGenerator)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import io.github.typesafegithub.workflows.domain.Job
import io.github.typesafegithub.workflows.domain.JobOutputs
import io.github.typesafegithub.workflows.domain.RunnerType.UbuntuLatest
import io.github.typesafegithub.workflows.domain.actions.CustomAction
import io.github.typesafegithub.workflows.domain.actions.RegularAction
import io.github.typesafegithub.workflows.dsl.WorkflowBuilder
import io.github.typesafegithub.workflows.dsl.expressions.expr
import io.github.typesafegithub.workflows.internal.relativeToAbsolute
import java.nio.file.Path
import kotlin.io.path.invariantSeparatorsPathString
import kotlin.reflect.KParameter
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.isAccessible

@Suppress("LongMethod")
internal fun WorkflowBuilder.consistencyCheckJob(
Expand Down Expand Up @@ -37,6 +41,16 @@ internal fun WorkflowBuilder.consistencyCheckJob(
condition = consistencyCheckJobConfig.condition,
env = consistencyCheckJobConfig.env,
) {
val checkoutActionVersion =
when (consistencyCheckJobConfig.checkoutActionVersion) {
CheckoutActionVersionSource.BundledWithLibrary -> "v4"
is CheckoutActionVersionSource.Given -> consistencyCheckJobConfig.checkoutActionVersion.version
is CheckoutActionVersionSource.InferFromClasspath ->
inferCheckoutActionVersionFromClasspath(
consistencyCheckJobConfig.checkoutActionVersion.checkoutActionClassFQN,
)
}

uses(
name = "Check out",
// Since this action is used in a simple way, and we actually don't want to update the version
Expand All @@ -46,7 +60,7 @@ internal fun WorkflowBuilder.consistencyCheckJob(
CustomAction(
actionOwner = "actions",
actionName = "checkout",
actionVersion = "v4",
actionVersion = checkoutActionVersion,
),
)

Expand Down Expand Up @@ -99,3 +113,26 @@ internal fun WorkflowBuilder.consistencyCheckJob(
)
}
}

private fun inferCheckoutActionVersionFromClasspath(checkoutActionClassFQN: String): String {
val clazz: Class<*> =
try {
Class.forName(checkoutActionClassFQN)
} catch (_: ClassNotFoundException) {
error(
"actions/checkout is not found in the classpath! " +
"Either add a dependency on it (`@file:DependsOn(\"actions:checkout:<version>\")`), " +
"or don't use CheckoutActionVersionSource.InferFromClasspath()",
)
} as Class<*>
// It's easier to call the primary constructor, even though it's private, because
// the public constructor requires named arguments, and I'm not sure how to call
// it with these.
val constructor =
clazz.kotlin.primaryConstructor!!.also {
it.isAccessible = true
}
val args: Map<KParameter, Any?> = constructor.parameters.associateWith { null }
val bindingObject = constructor.callBy(args) as RegularAction<*>
return bindingObject.actionVersion
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public val DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG: ConsistencyCheckJobConfig.Confi
ConsistencyCheckJobConfig.Configuration(
condition = null,
env = emptyMap(),
checkoutActionVersion = CheckoutActionVersionSource.BundledWithLibrary,
additionalSteps = null,
useLocalBindingsServerAsFallback = false,
)
Expand All @@ -17,6 +18,11 @@ public sealed interface ConsistencyCheckJobConfig {
public data class Configuration(
val condition: String?,
val env: Map<String, String>,
/**
* Configures what version of https://github.com/actions/checkout is used in the consistency check job.
* Lets the user choose between convenience of automatic updates and more determinism and control if required.
*/
val checkoutActionVersion: CheckoutActionVersionSource,
val additionalSteps: (JobBuilder<JobOutputs.EMPTY>.() -> Unit)?,
/**
* If the script execution step in the consistency check job fails, another attempt to execute is made with a
Expand All @@ -27,3 +33,36 @@ public sealed interface ConsistencyCheckJobConfig {
val useLocalBindingsServerAsFallback: Boolean,
) : ConsistencyCheckJobConfig
}

public sealed interface CheckoutActionVersionSource {
/**
* Every version of github-workflows-kt library comes with a hardcoded version of this action.
* Since bumping this version is possible only upon major releases (major version bumps), this
* version often lags behind the newest one.
*
* This is the current default option.
*/
public object BundledWithLibrary : CheckoutActionVersionSource

/**
* In the great majority of workflows, the checkout action is used in a preferred version.
* We can utilize this fact and use the same version in the consistency check job.
* Warning: this option won't work if a dependency on "action:checkout:vN" is not present.
*
* This is the most convenient option, and a candidate to become the default one.
*/
public class InferFromClasspath(
/**
* Specifies the fully qualified name of a binding class for actions/checkout. Can be overridden if a custom
* binding class is provided for this action, under a different FQN.
*/
public val checkoutActionClassFQN: String = "io.github.typesafegithub.workflows.actions.actions.Checkout",
) : CheckoutActionVersionSource

/**
* Useful if it's desired to specify a concrete version by hand, right in your workflow.
*/
public class Given(
public val version: String,
) : CheckoutActionVersionSource
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import io.github.typesafegithub.workflows.actions.endbug.AddAndCommit
import io.github.typesafegithub.workflows.annotations.ExperimentalKotlinLogicStep
import io.github.typesafegithub.workflows.domain.Concurrency
import io.github.typesafegithub.workflows.domain.RunnerType
import io.github.typesafegithub.workflows.domain.actions.Action
import io.github.typesafegithub.workflows.domain.actions.Action.Outputs
import io.github.typesafegithub.workflows.domain.actions.RegularAction
import io.github.typesafegithub.workflows.domain.triggers.Push
import io.github.typesafegithub.workflows.dsl.expressions.expr
import io.github.typesafegithub.workflows.dsl.workflow
import io.github.typesafegithub.workflows.yaml.CheckoutActionVersionSource
import io.github.typesafegithub.workflows.yaml.ConsistencyCheckJobConfig.Disabled
import io.github.typesafegithub.workflows.yaml.DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG
import io.github.typesafegithub.workflows.yaml.Preamble.Just
Expand Down Expand Up @@ -318,6 +318,154 @@ class IntegrationTest :
""".trimIndent()
}

test("actions/checkout's version given explicitly") {
// when
workflow(
name = "Test workflow",
on = listOf(Push()),
sourceFile = sourceTempFile,
consistencyCheckJobConfig =
DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG.copy(
checkoutActionVersion = CheckoutActionVersionSource.Given("v123"),
),
) {
job(
id = "test_job",
runsOn = RunnerType.UbuntuLatest,
) {
run(
name = "Hello world!",
command = "echo 'hello!'",
)
}
}

// then
targetTempFile.readText() shouldBe
"""
# This file was generated using Kotlin DSL (.github/workflows/some_workflow.main.kts).
# If you want to modify the workflow, please change the Kotlin file and regenerate this YAML file.
# Generated with https://github.com/typesafegithub/github-workflows-kt

name: 'Test workflow'
on:
push: {}
jobs:
check_yaml_consistency:
name: 'Check YAML consistency'
runs-on: 'ubuntu-latest'
steps:
- id: 'step-0'
name: 'Check out'
uses: 'actions/checkout@v123'
- id: 'step-1'
name: 'Execute script'
run: 'rm ''.github/workflows/some_workflow.yaml'' && ''.github/workflows/some_workflow.main.kts'''
- id: 'step-2'
name: 'Consistency check'
run: 'git diff --exit-code ''.github/workflows/some_workflow.yaml'''
test_job:
runs-on: 'ubuntu-latest'
needs:
- 'check_yaml_consistency'
steps:
- id: 'step-0'
name: 'Hello world!'
run: 'echo ''hello!'''

""".trimIndent()
}

test("actions/checkout's version inferred from classpath") {
// when
workflow(
name = "Test workflow",
on = listOf(Push()),
sourceFile = sourceTempFile,
consistencyCheckJobConfig =
DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG.copy(
checkoutActionVersion = CheckoutActionVersionSource.InferFromClasspath(),
),
) {
job(
id = "test_job",
runsOn = RunnerType.UbuntuLatest,
) {
run(
name = "Hello world!",
command = "echo 'hello!'",
)
}
}

// then
targetTempFile.readText() shouldBe
"""
# This file was generated using Kotlin DSL (.github/workflows/some_workflow.main.kts).
# If you want to modify the workflow, please change the Kotlin file and regenerate this YAML file.
# Generated with https://github.com/typesafegithub/github-workflows-kt

name: 'Test workflow'
on:
push: {}
jobs:
check_yaml_consistency:
name: 'Check YAML consistency'
runs-on: 'ubuntu-latest'
steps:
- id: 'step-0'
name: 'Check out'
uses: 'actions/checkout@v4'
- id: 'step-1'
name: 'Execute script'
run: 'rm ''.github/workflows/some_workflow.yaml'' && ''.github/workflows/some_workflow.main.kts'''
- id: 'step-2'
name: 'Consistency check'
run: 'git diff --exit-code ''.github/workflows/some_workflow.yaml'''
test_job:
runs-on: 'ubuntu-latest'
needs:
- 'check_yaml_consistency'
steps:
- id: 'step-0'
name: 'Hello world!'
run: 'echo ''hello!'''

""".trimIndent()
}

test("actions/checkout's version inferred from classpath but binding class is not available") {
// when
shouldThrow<IllegalStateException> {
workflow(
name = "Test workflow",
on = listOf(Push()),
sourceFile = sourceTempFile,
consistencyCheckJobConfig =
DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG.copy(
checkoutActionVersion =
CheckoutActionVersionSource.InferFromClasspath(
checkoutActionClassFQN = "does.not.Exist",
),
),
) {
job(
id = "test_job",
runsOn = RunnerType.UbuntuLatest,
) {
run(
name = "Hello world!",
command = "echo 'hello!'",
)
}
}
}.also {
it.message shouldBe "actions/checkout is not found in the classpath! " +
"Either add a dependency on it (`@file:DependsOn(\"actions:checkout:<version>\")`), " +
"or don't use CheckoutActionVersionSource.InferFromClasspath()"
}
}

test("with concurrency, default behavior") {
// when
workflow(
Expand Down
Loading