Skip to content

Commit 661c6d6

Browse files
authored
feat(library): allow specifying checkout action's version in consistency check job (#2169)
Part of #2157. Deployed in another repo in typesafegithub/github-actions-typing#438.
1 parent abbaa77 commit 661c6d6

File tree

5 files changed

+253
-7
lines changed

5 files changed

+253
-7
lines changed

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3237,19 +3237,40 @@ public final class io/github/typesafegithub/workflows/yaml/CaseKt {
32373237
public static final fun snakeCaseOf (Ljava/lang/String;)Ljava/lang/String;
32383238
}
32393239

3240+
public abstract interface class io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource {
3241+
}
3242+
3243+
public final class io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$BundledWithLibrary : io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource {
3244+
public static final field INSTANCE Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$BundledWithLibrary;
3245+
}
3246+
3247+
public final class io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$Given : io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource {
3248+
public fun <init> (Ljava/lang/String;)V
3249+
public final fun getVersion ()Ljava/lang/String;
3250+
}
3251+
3252+
public final class io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource$InferFromClasspath : io/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource {
3253+
public fun <init> ()V
3254+
public fun <init> (Ljava/lang/String;)V
3255+
public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
3256+
public final fun getCheckoutActionClassFQN ()Ljava/lang/String;
3257+
}
3258+
32403259
public abstract interface class io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig {
32413260
}
32423261

32433262
public final class io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig$Configuration : io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig {
3244-
public fun <init> (Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Z)V
3263+
public fun <init> (Ljava/lang/String;Ljava/util/Map;Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource;Lkotlin/jvm/functions/Function1;Z)V
32453264
public final fun component1 ()Ljava/lang/String;
32463265
public final fun component2 ()Ljava/util/Map;
3247-
public final fun component3 ()Lkotlin/jvm/functions/Function1;
3248-
public final fun component4 ()Z
3249-
public final fun copy (Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Z)Lio/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig$Configuration;
3250-
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;
3266+
public final fun component3 ()Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource;
3267+
public final fun component4 ()Lkotlin/jvm/functions/Function1;
3268+
public final fun component5 ()Z
3269+
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;
3270+
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;
32513271
public fun equals (Ljava/lang/Object;)Z
32523272
public final fun getAdditionalSteps ()Lkotlin/jvm/functions/Function1;
3273+
public final fun getCheckoutActionVersion ()Lio/github/typesafegithub/workflows/yaml/CheckoutActionVersionSource;
32533274
public final fun getCondition ()Ljava/lang/String;
32543275
public final fun getEnv ()Ljava/util/Map;
32553276
public final fun getUseLocalBindingsServerAsFallback ()Z

github-workflows-kt/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dependencies {
2626
implementation("it.krzeminski:snakeyaml-engine-kmp:4.0.1")
2727
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.9.0")
2828
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
29+
implementation(kotlin("reflect"))
2930
implementation(projects.sharedInternal)
3031
ksp(projects.codeGenerator)
3132

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@ import io.github.typesafegithub.workflows.domain.Job
55
import io.github.typesafegithub.workflows.domain.JobOutputs
66
import io.github.typesafegithub.workflows.domain.RunnerType.UbuntuLatest
77
import io.github.typesafegithub.workflows.domain.actions.CustomAction
8+
import io.github.typesafegithub.workflows.domain.actions.RegularAction
89
import io.github.typesafegithub.workflows.dsl.WorkflowBuilder
910
import io.github.typesafegithub.workflows.dsl.expressions.expr
1011
import io.github.typesafegithub.workflows.internal.relativeToAbsolute
1112
import java.nio.file.Path
1213
import kotlin.io.path.invariantSeparatorsPathString
14+
import kotlin.reflect.KParameter
15+
import kotlin.reflect.full.primaryConstructor
16+
import kotlin.reflect.jvm.isAccessible
1317

1418
@Suppress("LongMethod")
1519
internal fun WorkflowBuilder.consistencyCheckJob(
@@ -37,6 +41,16 @@ internal fun WorkflowBuilder.consistencyCheckJob(
3741
condition = consistencyCheckJobConfig.condition,
3842
env = consistencyCheckJobConfig.env,
3943
) {
44+
val checkoutActionVersion =
45+
when (consistencyCheckJobConfig.checkoutActionVersion) {
46+
CheckoutActionVersionSource.BundledWithLibrary -> "v4"
47+
is CheckoutActionVersionSource.Given -> consistencyCheckJobConfig.checkoutActionVersion.version
48+
is CheckoutActionVersionSource.InferFromClasspath ->
49+
inferCheckoutActionVersionFromClasspath(
50+
consistencyCheckJobConfig.checkoutActionVersion.checkoutActionClassFQN,
51+
)
52+
}
53+
4054
uses(
4155
name = "Check out",
4256
// Since this action is used in a simple way, and we actually don't want to update the version
@@ -46,7 +60,7 @@ internal fun WorkflowBuilder.consistencyCheckJob(
4660
CustomAction(
4761
actionOwner = "actions",
4862
actionName = "checkout",
49-
actionVersion = "v4",
63+
actionVersion = checkoutActionVersion,
5064
),
5165
)
5266

@@ -99,3 +113,26 @@ internal fun WorkflowBuilder.consistencyCheckJob(
99113
)
100114
}
101115
}
116+
117+
private fun inferCheckoutActionVersionFromClasspath(checkoutActionClassFQN: String): String {
118+
val clazz: Class<*> =
119+
try {
120+
Class.forName(checkoutActionClassFQN)
121+
} catch (_: ClassNotFoundException) {
122+
error(
123+
"actions/checkout is not found in the classpath! " +
124+
"Either add a dependency on it (`@file:DependsOn(\"actions:checkout:<version>\")`), " +
125+
"or don't use CheckoutActionVersionSource.InferFromClasspath()",
126+
)
127+
} as Class<*>
128+
// It's easier to call the primary constructor, even though it's private, because
129+
// the public constructor requires named arguments, and I'm not sure how to call
130+
// it with these.
131+
val constructor =
132+
clazz.kotlin.primaryConstructor!!.also {
133+
it.isAccessible = true
134+
}
135+
val args: Map<KParameter, Any?> = constructor.parameters.associateWith { null }
136+
val bindingObject = constructor.callBy(args) as RegularAction<*>
137+
return bindingObject.actionVersion
138+
}

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ public val DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG: ConsistencyCheckJobConfig.Confi
77
ConsistencyCheckJobConfig.Configuration(
88
condition = null,
99
env = emptyMap(),
10+
checkoutActionVersion = CheckoutActionVersionSource.BundledWithLibrary,
1011
additionalSteps = null,
1112
useLocalBindingsServerAsFallback = false,
1213
)
@@ -17,6 +18,11 @@ public sealed interface ConsistencyCheckJobConfig {
1718
public data class Configuration(
1819
val condition: String?,
1920
val env: Map<String, String>,
21+
/**
22+
* Configures what version of https://github.com/actions/checkout is used in the consistency check job.
23+
* Lets the user choose between convenience of automatic updates and more determinism and control if required.
24+
*/
25+
val checkoutActionVersion: CheckoutActionVersionSource,
2026
val additionalSteps: (JobBuilder<JobOutputs.EMPTY>.() -> Unit)?,
2127
/**
2228
* If the script execution step in the consistency check job fails, another attempt to execute is made with a
@@ -27,3 +33,36 @@ public sealed interface ConsistencyCheckJobConfig {
2733
val useLocalBindingsServerAsFallback: Boolean,
2834
) : ConsistencyCheckJobConfig
2935
}
36+
37+
public sealed interface CheckoutActionVersionSource {
38+
/**
39+
* Every version of github-workflows-kt library comes with a hardcoded version of this action.
40+
* Since bumping this version is possible only upon major releases (major version bumps), this
41+
* version often lags behind the newest one.
42+
*
43+
* This is the current default option.
44+
*/
45+
public object BundledWithLibrary : CheckoutActionVersionSource
46+
47+
/**
48+
* In the great majority of workflows, the checkout action is used in a preferred version.
49+
* We can utilize this fact and use the same version in the consistency check job.
50+
* Warning: this option won't work if a dependency on "action:checkout:vN" is not present.
51+
*
52+
* This is the most convenient option, and a candidate to become the default one.
53+
*/
54+
public class InferFromClasspath(
55+
/**
56+
* Specifies the fully qualified name of a binding class for actions/checkout. Can be overridden if a custom
57+
* binding class is provided for this action, under a different FQN.
58+
*/
59+
public val checkoutActionClassFQN: String = "io.github.typesafegithub.workflows.actions.actions.Checkout",
60+
) : CheckoutActionVersionSource
61+
62+
/**
63+
* Useful if it's desired to specify a concrete version by hand, right in your workflow.
64+
*/
65+
public class Given(
66+
public val version: String,
67+
) : CheckoutActionVersionSource
68+
}

github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/IntegrationTest.kt

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import io.github.typesafegithub.workflows.actions.endbug.AddAndCommit
66
import io.github.typesafegithub.workflows.annotations.ExperimentalKotlinLogicStep
77
import io.github.typesafegithub.workflows.domain.Concurrency
88
import io.github.typesafegithub.workflows.domain.RunnerType
9-
import io.github.typesafegithub.workflows.domain.actions.Action
109
import io.github.typesafegithub.workflows.domain.actions.Action.Outputs
1110
import io.github.typesafegithub.workflows.domain.actions.RegularAction
1211
import io.github.typesafegithub.workflows.domain.triggers.Push
1312
import io.github.typesafegithub.workflows.dsl.expressions.expr
1413
import io.github.typesafegithub.workflows.dsl.workflow
14+
import io.github.typesafegithub.workflows.yaml.CheckoutActionVersionSource
1515
import io.github.typesafegithub.workflows.yaml.ConsistencyCheckJobConfig.Disabled
1616
import io.github.typesafegithub.workflows.yaml.DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG
1717
import io.github.typesafegithub.workflows.yaml.Preamble.Just
@@ -318,6 +318,154 @@ class IntegrationTest :
318318
""".trimIndent()
319319
}
320320

321+
test("actions/checkout's version given explicitly") {
322+
// when
323+
workflow(
324+
name = "Test workflow",
325+
on = listOf(Push()),
326+
sourceFile = sourceTempFile,
327+
consistencyCheckJobConfig =
328+
DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG.copy(
329+
checkoutActionVersion = CheckoutActionVersionSource.Given("v123"),
330+
),
331+
) {
332+
job(
333+
id = "test_job",
334+
runsOn = RunnerType.UbuntuLatest,
335+
) {
336+
run(
337+
name = "Hello world!",
338+
command = "echo 'hello!'",
339+
)
340+
}
341+
}
342+
343+
// then
344+
targetTempFile.readText() shouldBe
345+
"""
346+
# This file was generated using Kotlin DSL (.github/workflows/some_workflow.main.kts).
347+
# If you want to modify the workflow, please change the Kotlin file and regenerate this YAML file.
348+
# Generated with https://github.com/typesafegithub/github-workflows-kt
349+
350+
name: 'Test workflow'
351+
on:
352+
push: {}
353+
jobs:
354+
check_yaml_consistency:
355+
name: 'Check YAML consistency'
356+
runs-on: 'ubuntu-latest'
357+
steps:
358+
- id: 'step-0'
359+
name: 'Check out'
360+
uses: 'actions/checkout@v123'
361+
- id: 'step-1'
362+
name: 'Execute script'
363+
run: 'rm ''.github/workflows/some_workflow.yaml'' && ''.github/workflows/some_workflow.main.kts'''
364+
- id: 'step-2'
365+
name: 'Consistency check'
366+
run: 'git diff --exit-code ''.github/workflows/some_workflow.yaml'''
367+
test_job:
368+
runs-on: 'ubuntu-latest'
369+
needs:
370+
- 'check_yaml_consistency'
371+
steps:
372+
- id: 'step-0'
373+
name: 'Hello world!'
374+
run: 'echo ''hello!'''
375+
376+
""".trimIndent()
377+
}
378+
379+
test("actions/checkout's version inferred from classpath") {
380+
// when
381+
workflow(
382+
name = "Test workflow",
383+
on = listOf(Push()),
384+
sourceFile = sourceTempFile,
385+
consistencyCheckJobConfig =
386+
DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG.copy(
387+
checkoutActionVersion = CheckoutActionVersionSource.InferFromClasspath(),
388+
),
389+
) {
390+
job(
391+
id = "test_job",
392+
runsOn = RunnerType.UbuntuLatest,
393+
) {
394+
run(
395+
name = "Hello world!",
396+
command = "echo 'hello!'",
397+
)
398+
}
399+
}
400+
401+
// then
402+
targetTempFile.readText() shouldBe
403+
"""
404+
# This file was generated using Kotlin DSL (.github/workflows/some_workflow.main.kts).
405+
# If you want to modify the workflow, please change the Kotlin file and regenerate this YAML file.
406+
# Generated with https://github.com/typesafegithub/github-workflows-kt
407+
408+
name: 'Test workflow'
409+
on:
410+
push: {}
411+
jobs:
412+
check_yaml_consistency:
413+
name: 'Check YAML consistency'
414+
runs-on: 'ubuntu-latest'
415+
steps:
416+
- id: 'step-0'
417+
name: 'Check out'
418+
uses: 'actions/checkout@v4'
419+
- id: 'step-1'
420+
name: 'Execute script'
421+
run: 'rm ''.github/workflows/some_workflow.yaml'' && ''.github/workflows/some_workflow.main.kts'''
422+
- id: 'step-2'
423+
name: 'Consistency check'
424+
run: 'git diff --exit-code ''.github/workflows/some_workflow.yaml'''
425+
test_job:
426+
runs-on: 'ubuntu-latest'
427+
needs:
428+
- 'check_yaml_consistency'
429+
steps:
430+
- id: 'step-0'
431+
name: 'Hello world!'
432+
run: 'echo ''hello!'''
433+
434+
""".trimIndent()
435+
}
436+
437+
test("actions/checkout's version inferred from classpath but binding class is not available") {
438+
// when
439+
shouldThrow<IllegalStateException> {
440+
workflow(
441+
name = "Test workflow",
442+
on = listOf(Push()),
443+
sourceFile = sourceTempFile,
444+
consistencyCheckJobConfig =
445+
DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG.copy(
446+
checkoutActionVersion =
447+
CheckoutActionVersionSource.InferFromClasspath(
448+
checkoutActionClassFQN = "does.not.Exist",
449+
),
450+
),
451+
) {
452+
job(
453+
id = "test_job",
454+
runsOn = RunnerType.UbuntuLatest,
455+
) {
456+
run(
457+
name = "Hello world!",
458+
command = "echo 'hello!'",
459+
)
460+
}
461+
}
462+
}.also {
463+
it.message shouldBe "actions/checkout is not found in the classpath! " +
464+
"Either add a dependency on it (`@file:DependsOn(\"actions:checkout:<version>\")`), " +
465+
"or don't use CheckoutActionVersionSource.InferFromClasspath()"
466+
}
467+
}
468+
321469
test("with concurrency, default behavior") {
322470
// when
323471
workflow(

0 commit comments

Comments
 (0)