Repository: typesafegithub/github-workflows-kt
Pull request #1036 (2023-10-20 19:01:31)

[#941] feat(abg): allow generating script-specific code
#941

- [x] add tests


Patch:
diff --git a/automation/action-binding-generator/api/action-binding-generator.api b/automation/action-binding-generator/api/action-binding-generator.api
index bbb737615..6b69b3b41 100644
--- a/automation/action-binding-generator/api/action-binding-generator.api
+++ b/automation/action-binding-generator/api/action-binding-generator.api
@@ -57,8 +57,8 @@ public final class io/github/typesafegithub/workflows/actionbindinggenerator/Fro
 }
 
 public final class io/github/typesafegithub/workflows/actionbindinggenerator/GenerationKt {
-	public static final fun generateBinding (Lio/github/typesafegithub/workflows/actionbindinggenerator/ActionCoords;Lio/github/typesafegithub/workflows/actionbindinggenerator/MetadataRevision;ZLio/github/typesafegithub/workflows/actionbindinggenerator/Metadata;Ljava/util/Map;)Lio/github/typesafegithub/workflows/actionbindinggenerator/ActionBinding;
-	public static synthetic fun generateBinding$default (Lio/github/typesafegithub/workflows/actionbindinggenerator/ActionCoords;Lio/github/typesafegithub/workflows/actionbindinggenerator/MetadataRevision;ZLio/github/typesafegithub/workflows/actionbindinggenerator/Metadata;Ljava/util/Map;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/actionbindinggenerator/ActionBinding;
+	public static final fun generateBinding (Lio/github/typesafegithub/workflows/actionbindinggenerator/ActionCoords;Lio/github/typesafegithub/workflows/actionbindinggenerator/MetadataRevision;ZLio/github/typesafegithub/workflows/actionbindinggenerator/Metadata;Ljava/util/Map;Z)Lio/github/typesafegithub/workflows/actionbindinggenerator/ActionBinding;
+	public static synthetic fun generateBinding$default (Lio/github/typesafegithub/workflows/actionbindinggenerator/ActionCoords;Lio/github/typesafegithub/workflows/actionbindinggenerator/MetadataRevision;ZLio/github/typesafegithub/workflows/actionbindinggenerator/Metadata;Ljava/util/Map;ZILjava/lang/Object;)Lio/github/typesafegithub/workflows/actionbindinggenerator/ActionBinding;
 }
 
 public final class io/github/typesafegithub/workflows/actionbindinggenerator/Input {
diff --git a/automation/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/ClassNaming.kt b/automation/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/ClassNaming.kt
index 5a78011d6..f52524bc0 100644
--- a/automation/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/ClassNaming.kt
+++ b/automation/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/ClassNaming.kt
@@ -2,10 +2,14 @@ package io.github.typesafegithub.workflows.actionbindinggenerator
 
 import java.util.Locale
 
-internal fun ActionCoords.buildActionClassName(): String {
+internal fun ActionCoords.buildActionClassName(includeVersion: Boolean = true): String {
     val versionString =
-        version
-            .let { if (it.startsWith("v")) it else "v$it" }
-            .replaceFirstChar { it.titlecase(Locale.getDefault()) }
+        if (includeVersion) {
+            version
+                .let { if (it.startsWith("v")) it else "v$it" }
+                .replaceFirstChar { it.titlecase(Locale.getDefault()) }
+        } else {
+            ""
+        }
     return "${name.toPascalCase()}$versionString"
 }
diff --git a/automation/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/Generation.kt b/automation/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/Generation.kt
index 9a2b9bbb4..bfcf5599b 100644
--- a/automation/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/Generation.kt
+++ b/automation/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/Generation.kt
@@ -42,6 +42,7 @@ public fun ActionCoords.generateBinding(
     useCache: Boolean = true,
     metadata: Metadata = this.fetchMetadata(metadataRevision, useCache = useCache),
     inputTypings: Map<String, Typing> = provideTypes(metadataRevision),
+    generateForScript: Boolean = false,
 ): ActionBinding {
     require(this.version.removePrefix("v").toIntOrNull() != null) {
         "Only major versions are supported, and '${this.version}' was given!"
@@ -53,8 +54,9 @@ public fun ActionCoords.generateBinding(
         println("$prettyPrint I suggest the following typings:\n$formatSuggestions")
     }
 
-    val className = this.buildActionClassName()
-    val actionBindingSourceCode = generateActionBindingSourceCode(metadataProcessed, this, inputTypings, className)
+    val className = this.buildActionClassName(includeVersion = !generateForScript)
+    val actionBindingSourceCode =
+        generateActionBindingSourceCode(metadataProcessed, this, inputTypings, className, generateForScript = generateForScript)
     val packageName = owner.toKotlinPackageName()
     return ActionBinding(
         kotlinCode = actionBindingSourceCode,
@@ -95,21 +97,33 @@ private fun generateActionBindingSourceCode(
     coords: ActionCoords,
     inputTypings: Map<String, Typing>,
     className: String,
+    generateForScript: Boolean,
 ): String {
     val fileSpec =
-        FileSpec.builder("io.github.typesafegithub.workflows.actions.${coords.owner.toKotlinPackageName()}", className)
-            .addFileComment(
-                """
-                This file was generated using 'action-binding-generator' module. Don't change it by hand, your changes will
-                be overwritten with the next binding code regeneration. Instead, consider introducing changes to the
-                generator itself.
-                """.trimIndent(),
-            )
-            .addType(generateActionClass(metadata, coords, inputTypings, className))
+        FileSpec.builder(
+            if (generateForScript) "" else "io.github.typesafegithub.workflows.actions.${coords.owner.toKotlinPackageName()}",
+            className,
+        )
+            .apply {
+                if (!generateForScript) {
+                    addFileComment(
+                        """
+                        This file was generated using 'action-binding-generator' module. Don't change it by hand, your changes will
+                        be overwritten with the next binding code regeneration. Instead, consider introducing changes to the
+                        generator itself.
+                        """.trimIndent(),
+                    )
+                }
+            }
+            .addType(generateActionClass(metadata, coords, inputTypings, className, generateForScript = generateForScript))
             .addSuppressAnnotation(metadata, coords)
             .indent("    ")
             .build()
     return buildString {
+        if (generateForScript) {
+            appendLine("#!/usr/bin/env kotlin")
+            appendLine("@file:DependsOn(\"io.github.typesafegithub:github-workflows-kt:$LIBRARY_VERSION\")")
+        }
         fileSpec.writeTo(this)
     }
 }
@@ -139,12 +153,13 @@ private fun generateActionClass(
     coords: ActionCoords,
     inputTypings: Map<String, Typing>,
     className: String,
+    generateForScript: Boolean,
 ): TypeSpec {
     return TypeSpec.classBuilder(className)
         .addModifiers(KModifier.DATA)
         .addKdoc(actionKdoc(metadata, coords))
         .addMaybeDeprecated(coords)
-        .inheritsFromRegularAction(coords, metadata, className)
+        .inheritsFromRegularAction(coords, metadata, className, generateForScript = generateForScript)
         .primaryConstructor(metadata.primaryConstructor(inputTypings, coords, className))
         .properties(metadata, coords, inputTypings, className)
         .addFunction(metadata.secondaryConstructor(inputTypings, coords, className))
@@ -301,6 +316,7 @@ private fun TypeSpec.Builder.inheritsFromRegularAction(
     coords: ActionCoords,
     metadata: Metadata,
     className: String,
+    generateForScript: Boolean,
 ): TypeSpec.Builder {
     val superclass =
         ClassName("io.github.typesafegithub.workflows.domain.actions", "RegularAction")
@@ -309,7 +325,7 @@ private fun TypeSpec.Builder.inheritsFromRegularAction(
                     OutputsBase
                 } else {
                     ClassName(
-                        "io.github.typesafegithub.workflows.actions.${coords.owner.toKotlinPackageName()}",
+                        if (generateForScript) "" else "io.github.typesafegithub.workflows.actions.${coords.owner.toKotlinPackageName()}",
                         className,
                         "Outputs",
                     )
diff --git a/automation/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/LibraryVersion.kt b/automation/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/LibraryVersion.kt
new file mode 100644
index 000000000..de1638185
--- /dev/null
+++ b/automation/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/LibraryVersion.kt
@@ -0,0 +1,3 @@
+package io.github.typesafegithub.workflows.actionbindinggenerator
+
+internal const val LIBRARY_VERSION = "1.3.2-SNAPSHOT"
diff --git a/library/build.gradle.kts b/library/build.gradle.kts
index 2fafb51b7..86b79c427 100644
--- a/library/build.gradle.kts
+++ b/library/build.gradle.kts
@@ -74,6 +74,10 @@ val validateDuplicatedVersion by tasks.creating<Task> {
             project.rootDir.resolve("mkdocs.yml").readText()
                 .contains("  version: $version")
         ) { "Library version stated in the docs should be equal to $version!" }
+        require(
+            project.rootDir.resolve("automation/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/LibraryVersion.kt").readText()
+                .contains("internal const val LIBRARY_VERSION = \"$version\"")
+        ) { "Library version stated in automation/action-binding-generator/src/main/.../LibraryVersion.kt should be equal to $version!" }
         require(
             project.file("src/test/kotlin/io/github/typesafegithub/workflows/docsnippets/GettingStartedSnippets.kt").readText()
                 .contains("\"io.github.typesafegithub:github-workflows-kt:$version\"")


Test patch:
diff --git a/automation/action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/GenerationTest.kt b/automation/action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/GenerationTest.kt
index 4256a491b..979a950d2 100644
--- a/automation/action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/GenerationTest.kt
+++ b/automation/action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/GenerationTest.kt
@@ -2,6 +2,7 @@ package io.github.typesafegithub.workflows.actionbindinggenerator
 
 import io.kotest.assertions.throwables.shouldThrowAny
 import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
 import io.kotest.matchers.throwable.shouldHaveMessage
 
 class GenerationTest : FunSpec({
@@ -403,4 +404,121 @@ class GenerationTest : FunSpec({
         // then
         binding.shouldMatchFile("ActionWithInputsSharingTypeV3.kt")
     }
+
+    test("action binding generated to be used from a script") {
+        // given
+        val actionManifest =
+            Metadata(
+                name =
+                    """
+                    Do something cool
+                    and describe it in multiple lines
+                    """.trimIndent(),
+                description = "This is a test description that should be put in the KDoc comment for a class",
+                inputs =
+                    mapOf(
+                        "foo-bar" to
+                            Input(
+                                description = "Short description",
+                                required = true,
+                                default = null,
+                            ),
+                        "baz-goo" to
+                            Input(
+                                description =
+                                    """
+                                    Just another input
+                                    with multiline description
+                                    """.trimIndent(),
+                                deprecationMessage = "this is deprecated",
+                                required = true,
+                                default = null,
+                            ),
+                    ),
+            )
+        val coords = ActionCoords("john-smith", "action-for-script", "v3")
+
+        // when
+        val binding =
+            coords.generateBinding(
+                metadataRevision = FromLockfile,
+                metadata = actionManifest,
+                generateForScript = true,
+            )
+
+        // then
+        //language=kotlin
+        binding.kotlinCode shouldBe
+            """
+            #!/usr/bin/env kotlin
+            @file:DependsOn("io.github.typesafegithub:github-workflows-kt:1.3.2-SNAPSHOT")
+            @file:Suppress(
+                "DataClassPrivateConstructor",
+                "UNUSED_PARAMETER",
+                "DEPRECATION",
+            )
+
+            import io.github.typesafegithub.workflows.domain.actions.Action
+            import io.github.typesafegithub.workflows.domain.actions.RegularAction
+            import java.util.LinkedHashMap
+            import kotlin.Deprecated
+            import kotlin.String
+            import kotlin.Suppress
+            import kotlin.Unit
+            import kotlin.collections.Map
+            import kotlin.collections.toList
+            import kotlin.collections.toTypedArray
+
+            /**
+             * Action: Do something cool
+             * and describe it in multiple lines
+             *
+             * This is a test description that should be put in the KDoc comment for a class
+             *
+             * [Action on GitHub](https://github.com/john-smith/action-for-script)
+             */
+            public data class ActionForScript private constructor(
+                /**
+                 * Short description
+                 */
+                public val fooBar: String,
+                /**
+                 * Just another input
+                 * with multiline description
+                 */
+                @Deprecated("this is deprecated")
+                public val bazGoo: String,
+                /**
+                 * Type-unsafe map where you can put any inputs that are not yet supported by the binding
+                 */
+                public val _customInputs: Map<String, String> = mapOf(),
+                /**
+                 * Allows overriding action's version, for example to use a specific minor version, or a newer
+                 * version that the binding doesn't yet know about
+                 */
+                public val _customVersion: String? = null,
+            ) : RegularAction<Action.Outputs>("john-smith", "action-for-script", _customVersion ?: "v3") {
+                public constructor(
+                    vararg pleaseUseNamedArguments: Unit,
+                    fooBar: String,
+                    bazGoo: String,
+                    _customInputs: Map<String, String> = mapOf(),
+                    _customVersion: String? = null,
+                ) : this(fooBar=fooBar, bazGoo=bazGoo, _customInputs=_customInputs,
+                        _customVersion=_customVersion)
+
+                @Suppress("SpreadOperator")
+                override fun toYamlArguments(): LinkedHashMap<String, String> = linkedMapOf(
+                    *listOfNotNull(
+                        "foo-bar" to fooBar,
+                        "baz-goo" to bazGoo,
+                        *_customInputs.toList().toTypedArray(),
+                    ).toTypedArray()
+                )
+
+                override fun buildOutputObject(stepId: String): Action.Outputs = Outputs(stepId)
+            }
+
+            """.trimIndent()
+    }
 })