From 414e1e5647d8be2607d2b724237b0476891dee7f Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Tue, 16 Dec 2025 00:04:47 +0100 Subject: [PATCH 01/10] feat(library): allow specifying checkout action's version in consistency check job --- .../workflows/yaml/ConsistencyCheckJob.kt | 21 +++- .../yaml/ConsistencyCheckJobConfig.kt | 12 ++ .../workflows/IntegrationTest.kt | 118 +++++++++++++++++- 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt index c59b41e81..1ec87b185 100644 --- a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt +++ b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt @@ -37,6 +37,12 @@ internal fun WorkflowBuilder.consistencyCheckJob( condition = consistencyCheckJobConfig.condition, env = consistencyCheckJobConfig.env, ) { + val checkoutActionVersion = when (consistencyCheckJobConfig.checkoutActionVersion) { + CheckoutActionVersionSource.BundledWithLibrary -> "v4" + is CheckoutActionVersionSource.Given -> consistencyCheckJobConfig.checkoutActionVersion.version + CheckoutActionVersionSource.InferredFromClasspath -> inferCheckoutActionVersionFromClasspath() + } + uses( name = "Check out", // Since this action is used in a simple way, and we actually don't want to update the version @@ -46,7 +52,7 @@ internal fun WorkflowBuilder.consistencyCheckJob( CustomAction( actionOwner = "actions", actionName = "checkout", - actionVersion = "v4", + actionVersion = checkoutActionVersion, ), ) @@ -99,3 +105,16 @@ internal fun WorkflowBuilder.consistencyCheckJob( ) } } + +private fun inferCheckoutActionVersionFromClasspath(): String { + val clazz = Class.forName("io.github.typesafegithub.workflows.actions.actions.Checkout") + println("Constructors!") + clazz.declaredConstructors.forEach { + println(it) + } + // TODO: how to call the constructor with default args? + // Or: how to get the version? + val instance = clazz.declaredConstructors.first().newInstance() + val version = clazz.getDeclaredMethod("getVersion").invoke(instance) + return version as String +} diff --git a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt index e2eb6c03e..86cebe55d 100644 --- a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt +++ b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt @@ -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, ) @@ -17,6 +18,11 @@ public sealed interface ConsistencyCheckJobConfig { public data class Configuration( val condition: String?, val env: Map, + /** + * Configures what version of https://github.com/actions/checkout should be 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.() -> Unit)?, /** * If the script execution step in the consistency check job fails, another attempt to execute is made with a @@ -27,3 +33,9 @@ public sealed interface ConsistencyCheckJobConfig { val useLocalBindingsServerAsFallback: Boolean, ) : ConsistencyCheckJobConfig } + +public sealed interface CheckoutActionVersionSource { + public object BundledWithLibrary : CheckoutActionVersionSource + public object InferredFromClasspath : CheckoutActionVersionSource + public class Given(public val version: String) : CheckoutActionVersionSource +} diff --git a/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt b/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt index 51c8e8f5e..47b71a267 100644 --- a/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt +++ b/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt @@ -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 @@ -318,6 +318,122 @@ 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.InferredFromClasspath + ), + ) { + 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("with concurrency, default behavior") { // when workflow( From 2dc49e2873509ba65859fee11cdfe14e54bd867e Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Tue, 16 Dec 2025 00:09:16 +0100 Subject: [PATCH 02/10] Works in tests? --- .../workflows/yaml/ConsistencyCheckJob.kt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt index 1ec87b185..4071fac56 100644 --- a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt +++ b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt @@ -108,13 +108,6 @@ internal fun WorkflowBuilder.consistencyCheckJob( private fun inferCheckoutActionVersionFromClasspath(): String { val clazz = Class.forName("io.github.typesafegithub.workflows.actions.actions.Checkout") - println("Constructors!") - clazz.declaredConstructors.forEach { - println(it) - } - // TODO: how to call the constructor with default args? - // Or: how to get the version? - val instance = clazz.declaredConstructors.first().newInstance() - val version = clazz.getDeclaredMethod("getVersion").invoke(instance) - return version as String + val jarName = clazz.protectionDomain.codeSource.location.toString().substringAfterLast("/") + return jarName.substringAfterLast("-").substringBeforeLast(".") } From 0f6f08ad3e955792be0be965d5908c79bb343baa Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Tue, 16 Dec 2025 15:06:51 +0100 Subject: [PATCH 03/10] Formatting --- .../workflows/yaml/ConsistencyCheckJob.kt | 16 ++++++++++------ .../workflows/yaml/ConsistencyCheckJobConfig.kt | 6 +++++- .../typesafegithub/workflows/IntegrationTest.kt | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt index 4071fac56..cf9f96c99 100644 --- a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt +++ b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt @@ -37,11 +37,12 @@ internal fun WorkflowBuilder.consistencyCheckJob( condition = consistencyCheckJobConfig.condition, env = consistencyCheckJobConfig.env, ) { - val checkoutActionVersion = when (consistencyCheckJobConfig.checkoutActionVersion) { - CheckoutActionVersionSource.BundledWithLibrary -> "v4" - is CheckoutActionVersionSource.Given -> consistencyCheckJobConfig.checkoutActionVersion.version - CheckoutActionVersionSource.InferredFromClasspath -> inferCheckoutActionVersionFromClasspath() - } + val checkoutActionVersion = + when (consistencyCheckJobConfig.checkoutActionVersion) { + CheckoutActionVersionSource.BundledWithLibrary -> "v4" + is CheckoutActionVersionSource.Given -> consistencyCheckJobConfig.checkoutActionVersion.version + CheckoutActionVersionSource.InferredFromClasspath -> inferCheckoutActionVersionFromClasspath() + } uses( name = "Check out", @@ -108,6 +109,9 @@ internal fun WorkflowBuilder.consistencyCheckJob( private fun inferCheckoutActionVersionFromClasspath(): String { val clazz = Class.forName("io.github.typesafegithub.workflows.actions.actions.Checkout") - val jarName = clazz.protectionDomain.codeSource.location.toString().substringAfterLast("/") + val jarName = + clazz.protectionDomain.codeSource.location + .toString() + .substringAfterLast("/") return jarName.substringAfterLast("-").substringBeforeLast(".") } diff --git a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt index 86cebe55d..2d8a90d97 100644 --- a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt +++ b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt @@ -36,6 +36,10 @@ public sealed interface ConsistencyCheckJobConfig { public sealed interface CheckoutActionVersionSource { public object BundledWithLibrary : CheckoutActionVersionSource + public object InferredFromClasspath : CheckoutActionVersionSource - public class Given(public val version: String) : CheckoutActionVersionSource + + public class Given( + public val version: String, + ) : CheckoutActionVersionSource } diff --git a/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt b/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt index 47b71a267..5a003d979 100644 --- a/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt +++ b/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt @@ -384,7 +384,7 @@ class IntegrationTest : sourceFile = sourceTempFile, consistencyCheckJobConfig = DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG.copy( - checkoutActionVersion = CheckoutActionVersionSource.InferredFromClasspath + checkoutActionVersion = CheckoutActionVersionSource.InferredFromClasspath, ), ) { job( From b172a6dfd831cb2d3b2d20496c7c0fc489505f4d Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Tue, 16 Dec 2025 15:07:31 +0100 Subject: [PATCH 04/10] Update API dump --- .../api/github-workflows-kt.api | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/github-workflows-kt/api/github-workflows-kt.api b/github-workflows-kt/api/github-workflows-kt.api index de9be120b..3f22c1041 100644 --- a/github-workflows-kt/api/github-workflows-kt.api +++ b/github-workflows-kt/api/github-workflows-kt.api @@ -3237,19 +3237,37 @@ 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 (Ljava/lang/String;)V + public final fun getVersion ()Ljava/lang/String; +} + +public final class io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$InferredFromClasspath : io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource { + public static final field INSTANCE Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$InferredFromClasspath; +} + 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 (Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Z)V + public fun (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 From 04e7b465505ea328f8ecac65f2be136bb81b8440 Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Tue, 16 Dec 2025 18:51:09 +0100 Subject: [PATCH 05/10] Publish snapshot always --- .github/workflows/build.main.kts | 1 - .github/workflows/build.yaml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/build.main.kts b/.github/workflows/build.main.kts index 690dc7a19..dbb6144e7 100755 --- a/.github/workflows/build.main.kts +++ b/.github/workflows/build.main.kts @@ -67,7 +67,6 @@ workflow( id = "publish-snapshot", name = "Publish snapshot", runsOn = UbuntuLatest, - condition = expr { "${github.ref} == 'refs/heads/main'" }, env = mapOf( "ORG_GRADLE_PROJECT_sonatypeUsername" to expr("secrets.ORG_GRADLE_PROJECT_SONATYPEUSERNAME"), "ORG_GRADLE_PROJECT_sonatypePassword" to expr("secrets.ORG_GRADLE_PROJECT_SONATYPEPASSWORD"), diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a16747138..39e20302d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -103,7 +103,6 @@ jobs: env: ORG_GRADLE_PROJECT_sonatypeUsername: '${{ secrets.ORG_GRADLE_PROJECT_SONATYPEUSERNAME }}' ORG_GRADLE_PROJECT_sonatypePassword: '${{ secrets.ORG_GRADLE_PROJECT_SONATYPEPASSWORD }}' - if: '${{ github.ref == ''refs/heads/main'' }}' steps: - id: 'step-0' uses: 'actions/checkout@v5' From 3d6981a9aae080b9900ddda8ad33647c844a9a40 Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Wed, 17 Dec 2025 07:37:18 +0100 Subject: [PATCH 06/10] Revert "Publish snapshot always" This reverts commit 04e7b465505ea328f8ecac65f2be136bb81b8440. --- .github/workflows/build.main.kts | 1 + .github/workflows/build.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build.main.kts b/.github/workflows/build.main.kts index dbb6144e7..690dc7a19 100755 --- a/.github/workflows/build.main.kts +++ b/.github/workflows/build.main.kts @@ -67,6 +67,7 @@ workflow( id = "publish-snapshot", name = "Publish snapshot", runsOn = UbuntuLatest, + condition = expr { "${github.ref} == 'refs/heads/main'" }, env = mapOf( "ORG_GRADLE_PROJECT_sonatypeUsername" to expr("secrets.ORG_GRADLE_PROJECT_SONATYPEUSERNAME"), "ORG_GRADLE_PROJECT_sonatypePassword" to expr("secrets.ORG_GRADLE_PROJECT_SONATYPEPASSWORD"), diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 39e20302d..a16747138 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -103,6 +103,7 @@ jobs: env: ORG_GRADLE_PROJECT_sonatypeUsername: '${{ secrets.ORG_GRADLE_PROJECT_SONATYPEUSERNAME }}' ORG_GRADLE_PROJECT_sonatypePassword: '${{ secrets.ORG_GRADLE_PROJECT_SONATYPEPASSWORD }}' + if: '${{ github.ref == ''refs/heads/main'' }}' steps: - id: 'step-0' uses: 'actions/checkout@v5' From 0301fd06b90df1718b1e87a4af597ef8cdfa281c Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Wed, 17 Dec 2025 07:39:26 +0100 Subject: [PATCH 07/10] Add coment about a hack --- .../typesafegithub/workflows/yaml/ConsistencyCheckJob.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt index cf9f96c99..43bbbc8cb 100644 --- a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt +++ b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt @@ -109,6 +109,10 @@ internal fun WorkflowBuilder.consistencyCheckJob( private fun inferCheckoutActionVersionFromClasspath(): String { val clazz = Class.forName("io.github.typesafegithub.workflows.actions.actions.Checkout") + // HACK: Ideally we'd instantiate the binding class and ask it for version, however + // it turned out to be difficult due to default arguments in Kotlin. Using Java's + // reflection, the constructors require passing ~40 arguments. As a workaround, + // the version is extracted from JAR's name, like: 'checkout-v4.jar' -> 'v4'. val jarName = clazz.protectionDomain.codeSource.location .toString() From 34d8966d16a175720f5f45653a70868813e6388e Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Wed, 17 Dec 2025 08:07:57 +0100 Subject: [PATCH 08/10] Extract action version from binding's API --- github-workflows-kt/build.gradle.kts | 1 + .../workflows/yaml/ConsistencyCheckJob.kt | 24 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/github-workflows-kt/build.gradle.kts b/github-workflows-kt/build.gradle.kts index 316be19e4..0d87ce296 100644 --- a/github-workflows-kt/build.gradle.kts +++ b/github-workflows-kt/build.gradle.kts @@ -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) diff --git a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt index 43bbbc8cb..8b3eaf46c 100644 --- a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt +++ b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt @@ -5,11 +5,16 @@ 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.memberProperties +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.isAccessible @Suppress("LongMethod") internal fun WorkflowBuilder.consistencyCheckJob( @@ -109,13 +114,14 @@ internal fun WorkflowBuilder.consistencyCheckJob( private fun inferCheckoutActionVersionFromClasspath(): String { val clazz = Class.forName("io.github.typesafegithub.workflows.actions.actions.Checkout") - // HACK: Ideally we'd instantiate the binding class and ask it for version, however - // it turned out to be difficult due to default arguments in Kotlin. Using Java's - // reflection, the constructors require passing ~40 arguments. As a workaround, - // the version is extracted from JAR's name, like: 'checkout-v4.jar' -> 'v4'. - val jarName = - clazz.protectionDomain.codeSource.location - .toString() - .substringAfterLast("/") - return jarName.substringAfterLast("-").substringBeforeLast(".") + // 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 = constructor.parameters.associateWith { null } + val bindingObject = constructor.callBy(args) as RegularAction<*> + return bindingObject.actionVersion } From b98f06ba5b89d30a07ce872885383f6cad0cde1f Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Wed, 17 Dec 2025 09:05:48 +0100 Subject: [PATCH 09/10] Rename option, refine docs, add tests --- .../api/github-workflows-kt.api | 4 +-- .../workflows/yaml/ConsistencyCheckJob.kt | 19 ++++++++--- .../yaml/ConsistencyCheckJobConfig.kt | 27 ++++++++++++++-- .../workflows/IntegrationTest.kt | 32 ++++++++++++++++++- 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/github-workflows-kt/api/github-workflows-kt.api b/github-workflows-kt/api/github-workflows-kt.api index 3f22c1041..6ce209ea1 100644 --- a/github-workflows-kt/api/github-workflows-kt.api +++ b/github-workflows-kt/api/github-workflows-kt.api @@ -3249,8 +3249,8 @@ public final class io/github/typesafegithub/workflows/yaml/CheckoutActionVersion public final fun getVersion ()Ljava/lang/String; } -public final class io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$InferredFromClasspath : io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource { - public static final field INSTANCE Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$InferredFromClasspath; +public final class io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$InferFromClasspath : io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource { + public static final field INSTANCE Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$InferFromClasspath; } public abstract interface class io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig { diff --git a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt index 8b3eaf46c..b33718b93 100644 --- a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt +++ b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt @@ -12,7 +12,6 @@ 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.memberProperties import kotlin.reflect.full.primaryConstructor import kotlin.reflect.jvm.isAccessible @@ -46,7 +45,10 @@ internal fun WorkflowBuilder.consistencyCheckJob( when (consistencyCheckJobConfig.checkoutActionVersion) { CheckoutActionVersionSource.BundledWithLibrary -> "v4" is CheckoutActionVersionSource.Given -> consistencyCheckJobConfig.checkoutActionVersion.version - CheckoutActionVersionSource.InferredFromClasspath -> inferCheckoutActionVersionFromClasspath() + CheckoutActionVersionSource.InferFromClasspath -> + inferCheckoutActionVersionFromClasspath( + consistencyCheckJobConfig.checkoutActionClassFQN, + ) } uses( @@ -112,8 +114,17 @@ internal fun WorkflowBuilder.consistencyCheckJob( } } -private fun inferCheckoutActionVersionFromClasspath(): String { - val clazz = Class.forName("io.github.typesafegithub.workflows.actions.actions.Checkout") +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:\")`), " + + "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. diff --git a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt index 2d8a90d97..1556f4d23 100644 --- a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt +++ b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt @@ -8,6 +8,7 @@ public val DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG: ConsistencyCheckJobConfig.Confi condition = null, env = emptyMap(), checkoutActionVersion = CheckoutActionVersionSource.BundledWithLibrary, + checkoutActionClassFQN = "io.github.typesafegithub.workflows.actions.actions.Checkout", additionalSteps = null, useLocalBindingsServerAsFallback = false, ) @@ -19,10 +20,15 @@ public sealed interface ConsistencyCheckJobConfig { val condition: String?, val env: Map, /** - * Configures what version of https://github.com/actions/checkout should be used in the consistency check job. + * 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, + /** + * 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. + */ + val checkoutActionClassFQN: String, val additionalSteps: (JobBuilder.() -> Unit)?, /** * If the script execution step in the consistency check job fails, another attempt to execute is made with a @@ -35,10 +41,27 @@ public sealed interface 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 - public object InferredFromClasspath : 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 object InferFromClasspath : 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 diff --git a/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt b/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt index 5a003d979..a28f07959 100644 --- a/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt +++ b/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt @@ -384,7 +384,7 @@ class IntegrationTest : sourceFile = sourceTempFile, consistencyCheckJobConfig = DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG.copy( - checkoutActionVersion = CheckoutActionVersionSource.InferredFromClasspath, + checkoutActionVersion = CheckoutActionVersionSource.InferFromClasspath, ), ) { job( @@ -434,6 +434,36 @@ class IntegrationTest : """.trimIndent() } + test("actions/checkout's version inferred from classpath but binding class is not available") { + // when + shouldThrow { + 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:\")`), " + + "or don't use CheckoutActionVersionSource.InferFromClasspath" + } + } + test("with concurrency, default behavior") { // when workflow( From fd36f3ed1a0953da410edda03222676d72634d77 Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Wed, 17 Dec 2025 09:17:32 +0100 Subject: [PATCH 10/10] Move checkoutActionClassFQN under InferFromClasspath --- github-workflows-kt/api/github-workflows-kt.api | 5 ++++- .../workflows/yaml/ConsistencyCheckJob.kt | 6 +++--- .../workflows/yaml/ConsistencyCheckJobConfig.kt | 14 +++++++------- .../typesafegithub/workflows/IntegrationTest.kt | 10 ++++++---- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/github-workflows-kt/api/github-workflows-kt.api b/github-workflows-kt/api/github-workflows-kt.api index 6ce209ea1..f7f25e4cc 100644 --- a/github-workflows-kt/api/github-workflows-kt.api +++ b/github-workflows-kt/api/github-workflows-kt.api @@ -3250,7 +3250,10 @@ public final class io/github/typesafegithub/workflows/yaml/CheckoutActionVersion } public final class io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$InferFromClasspath : io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource { - public static final field INSTANCE Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$InferFromClasspath; + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (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 { diff --git a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt index b33718b93..f82a222c5 100644 --- a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt +++ b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJob.kt @@ -45,9 +45,9 @@ internal fun WorkflowBuilder.consistencyCheckJob( when (consistencyCheckJobConfig.checkoutActionVersion) { CheckoutActionVersionSource.BundledWithLibrary -> "v4" is CheckoutActionVersionSource.Given -> consistencyCheckJobConfig.checkoutActionVersion.version - CheckoutActionVersionSource.InferFromClasspath -> + is CheckoutActionVersionSource.InferFromClasspath -> inferCheckoutActionVersionFromClasspath( - consistencyCheckJobConfig.checkoutActionClassFQN, + consistencyCheckJobConfig.checkoutActionVersion.checkoutActionClassFQN, ) } @@ -122,7 +122,7 @@ private fun inferCheckoutActionVersionFromClasspath(checkoutActionClassFQN: Stri error( "actions/checkout is not found in the classpath! " + "Either add a dependency on it (`@file:DependsOn(\"actions:checkout:\")`), " + - "or don't use CheckoutActionVersionSource.InferFromClasspath", + "or don't use CheckoutActionVersionSource.InferFromClasspath()", ) } as Class<*> // It's easier to call the primary constructor, even though it's private, because diff --git a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt index 1556f4d23..ec6437fd0 100644 --- a/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt +++ b/github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt @@ -8,7 +8,6 @@ public val DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG: ConsistencyCheckJobConfig.Confi condition = null, env = emptyMap(), checkoutActionVersion = CheckoutActionVersionSource.BundledWithLibrary, - checkoutActionClassFQN = "io.github.typesafegithub.workflows.actions.actions.Checkout", additionalSteps = null, useLocalBindingsServerAsFallback = false, ) @@ -24,11 +23,6 @@ public sealed interface ConsistencyCheckJobConfig { * Lets the user choose between convenience of automatic updates and more determinism and control if required. */ val checkoutActionVersion: CheckoutActionVersionSource, - /** - * 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. - */ - val checkoutActionClassFQN: String, val additionalSteps: (JobBuilder.() -> Unit)?, /** * If the script execution step in the consistency check job fails, another attempt to execute is made with a @@ -57,7 +51,13 @@ public sealed interface CheckoutActionVersionSource { * * This is the most convenient option, and a candidate to become the default one. */ - public object InferFromClasspath : CheckoutActionVersionSource + 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. diff --git a/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt b/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt index a28f07959..43568fbe4 100644 --- a/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt +++ b/github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt @@ -384,7 +384,7 @@ class IntegrationTest : sourceFile = sourceTempFile, consistencyCheckJobConfig = DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG.copy( - checkoutActionVersion = CheckoutActionVersionSource.InferFromClasspath, + checkoutActionVersion = CheckoutActionVersionSource.InferFromClasspath(), ), ) { job( @@ -443,8 +443,10 @@ class IntegrationTest : sourceFile = sourceTempFile, consistencyCheckJobConfig = DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG.copy( - checkoutActionVersion = CheckoutActionVersionSource.InferFromClasspath, - checkoutActionClassFQN = "does.not.Exist", + checkoutActionVersion = + CheckoutActionVersionSource.InferFromClasspath( + checkoutActionClassFQN = "does.not.Exist", + ), ), ) { job( @@ -460,7 +462,7 @@ class IntegrationTest : }.also { it.message shouldBe "actions/checkout is not found in the classpath! " + "Either add a dependency on it (`@file:DependsOn(\"actions:checkout:\")`), " + - "or don't use CheckoutActionVersionSource.InferFromClasspath" + "or don't use CheckoutActionVersionSource.InferFromClasspath()" } }