diff --git a/action-binding-generator/api/action-binding-generator.api b/action-binding-generator/api/action-binding-generator.api index 76660945c9..5b01747d41 100644 --- a/action-binding-generator/api/action-binding-generator.api +++ b/action-binding-generator/api/action-binding-generator.api @@ -1,16 +1,18 @@ public final class io/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords; - public static synthetic fun copy$default (Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords; + public static synthetic fun copy$default (Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords; public fun equals (Ljava/lang/Object;)Z public final fun getName ()Ljava/lang/String; public final fun getOwner ()Ljava/lang/String; public final fun getPath ()Ljava/lang/String; + public final fun getTypesUuid ()Ljava/lang/String; public final fun getVersion ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -46,6 +48,7 @@ public final class io/github/typesafegithub/workflows/actionbindinggenerator/dom public final class io/github/typesafegithub/workflows/actionbindinggenerator/domain/TypingActualSource : java/lang/Enum { public static final field ACTION Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/TypingActualSource; + public static final field CUSTOM Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/TypingActualSource; public static final field TYPING_CATALOG Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/TypingActualSource; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/TypingActualSource; @@ -72,8 +75,8 @@ public final class io/github/typesafegithub/workflows/actionbindinggenerator/gen } public final class io/github/typesafegithub/workflows/actionbindinggenerator/generation/GenerationKt { - public static final fun generateBinding (Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords;Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/MetadataRevision;Lio/github/typesafegithub/workflows/actionbindinggenerator/metadata/Metadata;Lkotlin/Pair;)Ljava/util/List; - public static synthetic fun generateBinding$default (Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords;Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/MetadataRevision;Lio/github/typesafegithub/workflows/actionbindinggenerator/metadata/Metadata;Lkotlin/Pair;ILjava/lang/Object;)Ljava/util/List; + public static final fun generateBinding (Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords;Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/MetadataRevision;Lio/github/typesafegithub/workflows/actionbindinggenerator/metadata/Metadata;Lkotlin/Pair;Ljava/lang/String;Ljava/lang/String;)Ljava/util/List; + public static synthetic fun generateBinding$default (Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords;Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/MetadataRevision;Lio/github/typesafegithub/workflows/actionbindinggenerator/metadata/Metadata;Lkotlin/Pair;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljava/util/List; } public final class io/github/typesafegithub/workflows/actionbindinggenerator/metadata/Input { @@ -146,8 +149,8 @@ public final class io/github/typesafegithub/workflows/actionbindinggenerator/met } public final class io/github/typesafegithub/workflows/actionbindinggenerator/metadata/MetadataReadingKt { - public static final fun fetchMetadata (Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords;Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/MetadataRevision;Lkotlin/jvm/functions/Function1;)Lio/github/typesafegithub/workflows/actionbindinggenerator/metadata/Metadata; - public static synthetic fun fetchMetadata$default (Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords;Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/MetadataRevision;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/actionbindinggenerator/metadata/Metadata; + public static final fun fetchMetadata (Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords;Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/MetadataRevision;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/github/typesafegithub/workflows/actionbindinggenerator/metadata/Metadata; + public static synthetic fun fetchMetadata$default (Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords;Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/MetadataRevision;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/actionbindinggenerator/metadata/Metadata; } public final class io/github/typesafegithub/workflows/actionbindinggenerator/metadata/Output { diff --git a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords.kt b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords.kt index 69205b6339..c53c5756cb 100644 --- a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords.kt +++ b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords.kt @@ -5,6 +5,7 @@ public data class ActionCoords( val name: String, val version: String, val path: String? = null, + val typesUuid: String? = null, ) /** @@ -13,7 +14,7 @@ public data class ActionCoords( */ public val ActionCoords.isTopLevel: Boolean get() = path == null -public val ActionCoords.prettyPrint: String get() = "$owner/$fullName@$version" +public val ActionCoords.prettyPrint: String get() = "$owner/$fullName@$version${typesUuid?.let { " (types: $it)" } ?: ""}" /** * For most actions, it's empty. diff --git a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/TypingActualSource.kt b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/TypingActualSource.kt index 094f3251b4..4a6fca4f9e 100644 --- a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/TypingActualSource.kt +++ b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/TypingActualSource.kt @@ -3,4 +3,5 @@ package io.github.typesafegithub.workflows.actionbindinggenerator.domain public enum class TypingActualSource { ACTION, TYPING_CATALOG, + CUSTOM, } diff --git a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/generation/Generation.kt b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/generation/Generation.kt index 070ab2b477..033c36f060 100644 --- a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/generation/Generation.kt +++ b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/generation/Generation.kt @@ -61,11 +61,14 @@ public fun ActionCoords.generateBinding( metadataRevision: MetadataRevision, metadata: Metadata? = null, inputTypings: Pair, TypingActualSource?>? = null, + types: String? = null, + explicitMetadata: String? = null, ): List { - val metadataResolved = metadata ?: this.fetchMetadata(metadataRevision) ?: return emptyList() + val metadataResolved = metadata ?: this.fetchMetadata(metadataRevision, explicitMetadata) ?: return emptyList() val metadataProcessed = metadataResolved.removeDeprecatedInputsIfNameClash() - val inputTypingsResolved = inputTypings ?: this.provideTypes(metadataRevision) + val (inputTypingsResolved, typingActualSource) = + inputTypings ?: this.provideTypes(metadataRevision, types = types) val packageName = owner.toKotlinPackageName() val className = this.buildActionClassName() @@ -78,7 +81,7 @@ public fun ActionCoords.generateBinding( emptyMap(), classNameUntyped, untypedClass = true, - replaceWith = inputTypingsResolved.second?.let { CodeBlock.of("ReplaceWith(%S)", className) }, + replaceWith = typingActualSource?.let { CodeBlock.of("ReplaceWith(%S)", className) }, ) return listOfNotNull( @@ -89,12 +92,12 @@ public fun ActionCoords.generateBinding( packageName = packageName, typingActualSource = null, ), - inputTypingsResolved.second?.let { + typingActualSource?.let { val actionBindingSourceCode = generateActionBindingSourceCode( metadata = metadataProcessed, coords = this, - inputTypings = inputTypingsResolved.first, + inputTypings = inputTypingsResolved, className = className, ) ActionBinding( diff --git a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/metadata/MetadataReading.kt b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/metadata/MetadataReading.kt index 815e720eab..7551beeb00 100644 --- a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/metadata/MetadataReading.kt +++ b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/metadata/MetadataReading.kt @@ -45,8 +45,13 @@ private fun ActionCoords.actionYamlUrl(gitRef: String) = "https://raw.githubuser public fun ActionCoords.fetchMetadata( metadataRevision: MetadataRevision, + explicitMetadata: String? = null, fetchUri: (URI) -> String = ::fetchUri, ): Metadata? { + if (explicitMetadata != null) { + return yaml.decodeFromString(explicitMetadata) + } + val gitRef = when (metadataRevision) { is CommitHash -> metadataRevision.value diff --git a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/typing/TypesProviding.kt b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/typing/TypesProviding.kt index 46e6eb6344..63477a3860 100644 --- a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/typing/TypesProviding.kt +++ b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/typing/TypesProviding.kt @@ -10,6 +10,7 @@ import io.github.typesafegithub.workflows.actionbindinggenerator.domain.Metadata import io.github.typesafegithub.workflows.actionbindinggenerator.domain.NewestForVersion import io.github.typesafegithub.workflows.actionbindinggenerator.domain.TypingActualSource import io.github.typesafegithub.workflows.actionbindinggenerator.domain.TypingActualSource.ACTION +import io.github.typesafegithub.workflows.actionbindinggenerator.domain.TypingActualSource.CUSTOM import io.github.typesafegithub.workflows.actionbindinggenerator.domain.TypingActualSource.TYPING_CATALOG import io.github.typesafegithub.workflows.actionbindinggenerator.domain.subName import io.github.typesafegithub.workflows.actionbindinggenerator.metadata.fetchUri @@ -24,10 +25,12 @@ private val logger = logger { } internal fun ActionCoords.provideTypes( metadataRevision: MetadataRevision, fetchUri: (URI) -> String = ::fetchUri, + types: String? = null, ): Pair, TypingActualSource?> = ( - this.fetchTypingMetadata(metadataRevision, fetchUri) - ?: this.toMajorVersion().fetchFromTypingsFromCatalog(fetchUri) + customTypingMetadata(types) + ?: this.fetchTypingMetadata(metadataRevision, fetchUri) + ?: this.toMajorVersion().fetchTypingsFromCatalog(fetchUri) )?.let { Pair(it.first.toTypesMap(), it.second) } ?: Pair(emptyMap(), null) @@ -45,6 +48,9 @@ private fun ActionCoords.catalogMetadata() = private fun ActionCoords.actionTypesYamlUrl(gitRef: String) = "https://raw.githubusercontent.com/$owner/$name/$gitRef$subName/action-types.yaml" +private fun customTypingMetadata(types: String? = null) = + types?.let { Pair(yaml.decodeFromStringOrDefaultIfEmpty(it, ActionTypes()), CUSTOM) } + private fun ActionCoords.fetchTypingMetadata( metadataRevision: MetadataRevision, fetchUri: (URI) -> String = ::fetchUri, @@ -68,7 +74,7 @@ private fun ActionCoords.fetchTypingMetadata( return Pair(yaml.decodeFromStringOrDefaultIfEmpty(typesMetadataYaml, ActionTypes()), ACTION) } -private fun ActionCoords.fetchFromTypingsFromCatalog(fetchUri: (URI) -> String = ::fetchUri): Pair? = +private fun ActionCoords.fetchTypingsFromCatalog(fetchUri: (URI) -> String = ::fetchUri): Pair? = ( fetchTypingsFromUrl(url = actionTypesFromCatalog(), fetchUri = fetchUri) ?: fetchTypingsForOlderVersionFromCatalog(fetchUri = fetchUri) diff --git a/action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/typing/TypesProvidingTest.kt b/action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/typing/TypesProvidingTest.kt index 9b06cd36be..5d7aaa9ba3 100644 --- a/action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/typing/TypesProvidingTest.kt +++ b/action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/typing/TypesProvidingTest.kt @@ -151,6 +151,12 @@ class TypesProvidingTest : stored-in-typing-catalog: type: string """.trimIndent() + val custom = + """ + inputs: + custom: + type: string + """.trimIndent() val metadata = """ "versionsWithTypings": @@ -375,6 +381,134 @@ class TypesProvidingTest : types shouldBe Pair(mapOf("hosted-by-action-yml" to StringTyping), TypingActualSource.ACTION) } + test("only custom") { + // Given + val fetchUri: (URI) -> String = { throw IOException() } + val actionCoord = ActionCoords("some-owner", "some-name", "v3") + + // When + val types = actionCoord.provideTypes(metadataRevision = CommitHash("some-hash"), fetchUri = fetchUri, types = custom) + + // Then + types shouldBe Pair(mapOf("custom" to StringTyping), TypingActualSource.CUSTOM) + } + + test("only custom for subaction") { + // Given + val fetchUri: (URI) -> String = { throw IOException() } + val actionCoord = ActionCoords("some-owner", "some-name", "v3", "some-sub") + + // When + val types = actionCoord.provideTypes(metadataRevision = CommitHash("some-hash"), fetchUri = fetchUri, types = custom) + + // Then + types shouldBe Pair(mapOf("custom" to StringTyping), TypingActualSource.CUSTOM) + } + + test("hosted by action, stored in typing catalog, and custom") { + // Given + val fetchUri: (URI) -> String = { + when (it) { + URI( + "https://raw.githubusercontent.com/some-owner/some-name/" + + "some-hash/action-types.yml", + ), + -> hostedByActionYml + URI( + "https://raw.githubusercontent.com/typesafegithub/github-actions-typing-catalog/" + + "main/typings/some-owner/some-name/v3/action-types.yml", + ), + -> storedInTypingCatalog + else -> throw IOException() + } + } + val actionCoord = ActionCoords("some-owner", "some-name", "v3") + + // When + val types = actionCoord.provideTypes(metadataRevision = CommitHash("some-hash"), fetchUri = fetchUri, types = custom) + + // Then + types shouldBe Pair(mapOf("custom" to StringTyping), TypingActualSource.CUSTOM) + } + + test("hosted by subaction, stored in typing catalog, and custom") { + // Given + val fetchUri: (URI) -> String = { + when (it) { + URI( + "https://raw.githubusercontent.com/some-owner/some-name/" + + "some-hash/some-sub/action-types.yml", + ), + -> hostedByActionYml + URI( + "https://raw.githubusercontent.com/typesafegithub/github-actions-typing-catalog/" + + "main/typings/some-owner/some-name/v3/some-sub/action-types.yml", + ), + -> storedInTypingCatalog + else -> throw IOException() + } + } + val actionCoord = ActionCoords("some-owner", "some-name", "v3", "some-sub") + + // When + val types = actionCoord.provideTypes(metadataRevision = CommitHash("some-hash"), fetchUri = fetchUri, types = custom) + + // Then + types shouldBe Pair(mapOf("custom" to StringTyping), TypingActualSource.CUSTOM) + } + + test("hosted by action, stored in typing catalog, and empty custom") { + // Given + val fetchUri: (URI) -> String = { + when (it) { + URI( + "https://raw.githubusercontent.com/some-owner/some-name/" + + "some-hash/action-types.yml", + ), + -> hostedByActionYml + URI( + "https://raw.githubusercontent.com/typesafegithub/github-actions-typing-catalog/" + + "main/typings/some-owner/some-name/v3/action-types.yml", + ), + -> storedInTypingCatalog + else -> throw IOException() + } + } + val actionCoord = ActionCoords("some-owner", "some-name", "v3") + + // When + val types = actionCoord.provideTypes(metadataRevision = CommitHash("some-hash"), fetchUri = fetchUri, types = "") + + // Then + types shouldBe Pair(emptyMap(), TypingActualSource.CUSTOM) + } + + test("hosted by subaction, stored in typing catalog, and empty custom") { + // Given + val fetchUri: (URI) -> String = { + when (it) { + URI( + "https://raw.githubusercontent.com/some-owner/some-name/" + + "some-hash/some-sub/action-types.yml", + ), + -> hostedByActionYml + URI( + "https://raw.githubusercontent.com/typesafegithub/github-actions-typing-catalog/" + + "main/typings/some-owner/some-name/v3/some-sub/action-types.yml", + ), + -> storedInTypingCatalog + else -> throw IOException() + } + } + val actionCoord = ActionCoords("some-owner", "some-name", "v3", "some-sub") + + // When + val types = actionCoord.provideTypes(metadataRevision = CommitHash("some-hash"), fetchUri = fetchUri, types = "") + + // Then + types shouldBe Pair(emptyMap(), TypingActualSource.CUSTOM) + } + test("only stored in typing catalog for older version") { // Given val fetchUri: (URI) -> String = { @@ -575,6 +709,26 @@ class TypesProvidingTest : ) } + test("only custom") { + // Given + val fetchUri: (URI) -> String = { throw IOException() } + val actionCoord = ActionCoords("some-owner", "some-name", "v3") + + // When + val types = actionCoord.provideTypes(metadataRevision = CommitHash("some-hash"), fetchUri = fetchUri, types = typingYml) + + // Then + types shouldBe + Pair( + mapOf( + "granted-scopes" to ListOfTypings(",", EnumTyping("GrantedScopes", listOf("read", "write"))), + "granted-scopes2" to ListOfTypings(",", EnumTyping("GrantedScopes", listOf("read", "write"))), + "granted-scopes3" to ListOfTypings("""\n""", EnumTyping("GrantedScopes", listOf("read", "write"))), + ), + TypingActualSource.CUSTOM, + ) + } + test("billion laughs attack is prevented") { // Given val billionLaughsAttack = diff --git a/docs/user-guide/using-actions.md b/docs/user-guide/using-actions.md index dc0079c1d1..8df10d66a1 100644 --- a/docs/user-guide/using-actions.md +++ b/docs/user-guide/using-actions.md @@ -40,6 +40,24 @@ There are two ways of configuring typings: a community-maintained place to host the typings. You can contribute or fix typings for your favorite action by sending a PR. +While developing a typing manifest it might be a good idea to test the result without needing to +release the action in question or merge a PR in the catalog. For this you can `POST` the typing manifest you have +on disk to the binding server using any valid URL for the action in question, for example using +```bash +curl -F types=@action-types.yml https://bindings.krzeminski.it/pbrisbin/setup-tool-action/v2/setup-tool-action-v2.pom +``` +The binding server generates a binding with only the given type manifest and answer with some unique coordinates +that you can use in a test workflow script. The binding will be available the normal cache time on the binding +server and locally as long as you do not delete it from your local Maven repository where it is cached. After +the cache period on the server ended requesting the same coordinates will return a binding as if no typing +information is available at all. + +When writing typings for a new action that is not published yet or a new version with changed inputs / outputs, +you should also provide the new action manifest, that the generation works with that state using +```bash +curl -F actionYaml=@action.yml -F types=@action-types.yml https://bindings.krzeminski.it/foo/bar/vX/bar-vX.pom +``` + Once there are any typings in place for the action, the `_Untyped` suffixed class is marked `@Deprecated`, and a class without that suffix is created additionally. In that class for each input that does not have type information available there will still be only the property with `_Untyped` suffix and nullability according to required status. For each diff --git a/jit-binding-server/build.gradle.kts b/jit-binding-server/build.gradle.kts index aa2aecb8c4..b00f7fac03 100644 --- a/jit-binding-server/build.gradle.kts +++ b/jit-binding-server/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation("io.ktor:ktor-serialization-kotlinx-json") implementation("io.ktor:ktor-server-call-logging") implementation("io.ktor:ktor-server-call-id") + implementation("it.krzeminski:snakeyaml-engine-kmp:3.0.2") implementation("io.ktor:ktor-server-metrics-micrometer") implementation("io.micrometer:micrometer-registry-prometheus:1.14.4") diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ActionCoords.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ActionCoords.kt index 37c7dbc0f4..bb50116873 100644 --- a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ActionCoords.kt +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ActionCoords.kt @@ -3,12 +3,24 @@ package io.github.typesafegithub.workflows.jitbindingserver import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords import io.ktor.http.Parameters -fun Parameters.extractActionCoords(extractVersion: Boolean): ActionCoords { - val owner = this["owner"]!! +fun Parameters.extractActionCoords( + extractVersion: Boolean, + owner: String = this["owner"]!!, +): ActionCoords { val nameAndPath = this["name"]!!.split("__") val name = nameAndPath.first() val path = nameAndPath.drop(1).joinToString("/").takeUnless { it.isBlank() } val version = if (extractVersion) this["version"]!! else "irrelevant" + // we cannot give the types UUID separately from the post handler + // only in the post handler we generate the UUID, but for the other + // handlers the UUID part is already coming through the request as part of the owner + val ownerAndTypesUuid = owner.split("__types__", limit = 2) + val ownerPlain = ownerAndTypesUuid.first() + val typesUuid = + ownerAndTypesUuid + .drop(1) + .takeIf { it.isNotEmpty() } + ?.single() - return ActionCoords(owner, name, version, path) + return ActionCoords(ownerPlain, name, version, path, typesUuid) } diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutes.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutes.kt index 9660352f48..df4000c09c 100644 --- a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutes.kt +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutes.kt @@ -12,20 +12,35 @@ import io.github.typesafegithub.workflows.mavenbinding.TextArtifact import io.github.typesafegithub.workflows.mavenbinding.buildVersionArtifacts import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode +import io.ktor.http.content.PartData +import io.ktor.http.content.asFlow +import io.ktor.http.parameters import io.ktor.server.application.ApplicationCall import io.ktor.server.request.httpMethod +import io.ktor.server.request.receiveMultipart +import io.ktor.server.request.receiveText import io.ktor.server.response.respondBytes import io.ktor.server.response.respondText import io.ktor.server.routing.Route import io.ktor.server.routing.Routing import io.ktor.server.routing.get import io.ktor.server.routing.head +import io.ktor.server.routing.post import io.ktor.server.routing.route +import io.ktor.utils.io.readRemaining +import io.ktor.utils.io.readText import io.micrometer.core.instrument.Tag import io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics import io.micrometer.prometheusmetrics.PrometheusMeterRegistry +import it.krzeminski.snakeyaml.engine.kmp.api.Load +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import java.util.UUID.randomUUID import kotlin.time.Duration.Companion.hours +private const val METADATA_PARAMETER = "actionYaml" +private const val TYPES_PARAMETER = "types" + private val logger = logger { } typealias ArtifactResult = Result> @@ -55,6 +70,7 @@ private fun Route.artifact( ) { headArtifact(prometheusRegistry, refresh) getArtifact(prometheusRegistry, refresh) + postArtifact(prometheusRegistry) } private fun Route.headArtifact( @@ -67,7 +83,7 @@ private fun Route.headArtifact( val file = call.parameters["file"] ?: return@head call.respondNotFound() if (file in bindingArtifacts) { - call.respondText("Exists", status = HttpStatusCode.OK) + call.respondText(text = "Exists", status = HttpStatusCode.OK) } else { call.respondNotFound() } @@ -83,14 +99,14 @@ private fun Route.getArtifact( get { val bindingArtifacts = call.toBindingArtifacts(refresh) ?: return@get call.respondNotFound() - if (refresh && !deliverOnRefreshRoute) return@get call.respondText("OK") + if (refresh && !deliverOnRefreshRoute) return@get call.respondText(text = "OK") val file = call.parameters["file"] ?: return@get call.respondNotFound() val artifact = bindingArtifacts[file] ?: return@get call.respondNotFound() when (artifact) { - is TextArtifact -> call.respondText(artifact.data()) + is TextArtifact -> call.respondText(text = artifact.data()) is JarArtifact -> call.respondBytes(artifact.data(), ContentType.parse("application/java-archive")) } @@ -98,16 +114,128 @@ private fun Route.getArtifact( } } -private suspend fun ApplicationCall.toBindingArtifacts(refresh: Boolean): Map? { - val actionCoords = parameters.extractActionCoords(extractVersion = true) +private fun Route.postArtifact(prometheusRegistry: PrometheusMeterRegistry) { + post { + val owner = "${call.parameters["owner"]}__types__${randomUUID()}" + val name = call.parameters["name"]!! + val version = call.parameters["version"]!! + + val (metadata, types) = + runCatching { + val parts = + call + .receiveMultipart() + .asFlow() + .map { + it.name to + runCatching { + when (it) { + is PartData.FileItem -> it.provider().readRemaining().readText() + is PartData.FormItem -> it.value + else -> { + logger.error { "Unexpected part data ${it::class.simpleName}" } + error("Unexpected part data ${it::class.simpleName}") + } + } + } + }.toList() + .map { (name, result) -> + name to + when { + result.isSuccess -> result.getOrThrow() + else -> { + call.respondText( + text = HttpStatusCode.InternalServerError.description, + status = HttpStatusCode.InternalServerError, + ) + return@post + } + } + }.associate { it } + + if (parts.keys.any { (it != METADATA_PARAMETER) && (it != TYPES_PARAMETER) }) { + call.respondText( + text = "Only '$METADATA_PARAMETER' and '$TYPES_PARAMETER' are allowed as form data fields", + status = HttpStatusCode.BadRequest, + ) + return@post + } + if (!parts.containsKey(TYPES_PARAMETER)) { + call.respondText( + text = "'$TYPES_PARAMETER' field is mandatory", + status = HttpStatusCode.BadRequest, + ) + return@post + } + parts[METADATA_PARAMETER] to parts[TYPES_PARAMETER]!! + }.recover { + null to call.receiveText() + }.getOrThrow() + + if (metadata != null) { + if (metadata.isEmpty()) { + call.respondText( + text = "Supplied $METADATA_PARAMETER is empty", + status = HttpStatusCode.UnprocessableEntity, + ) + return@post + } + + runCatching { + Load().loadOne(metadata) + }.onFailure { + call.respondText( + text = "Exception while parsing supplied $METADATA_PARAMETER:\n${it.stackTraceToString()}", + status = HttpStatusCode.UnprocessableEntity, + ) + return@post + } + } + + runCatching { + Load().loadOne(types) + }.onFailure { + call.respondText( + text = "Exception while parsing supplied $TYPES_PARAMETER:\n${it.stackTraceToString()}", + status = HttpStatusCode.UnprocessableEntity, + ) + return@post + } + + call.toBindingArtifacts(refresh = true, owner = owner, types = types, metadata = metadata) + call.respondText(text = "$owner:$name:$version") + + incrementArtifactCounter(prometheusRegistry, call) + } +} + +private suspend fun ApplicationCall.toBindingArtifacts( + refresh: Boolean, + owner: String = parameters["owner"]!!, + types: String? = null, + metadata: String? = null, +): Map? { + val actionCoords = parameters.extractActionCoords(extractVersion = true, owner = owner) logger.info { "➡️ Requesting ${actionCoords.prettyPrint}" } return if (refresh) { - actionCoords.buildVersionArtifacts().also { - bindingsCache.put(actionCoords, runCatching { it!! }) - } + actionCoords + .buildVersionArtifacts( + types ?: actionCoords.typesUuid?.let { "" }, + metadata, + ).also { + bindingsCache.put(actionCoords, runCatching { it!! }) + } } else { - bindingsCache.get(actionCoords) { runCatching { actionCoords.buildVersionArtifacts()!! } }.getOrNull() + bindingsCache + .get(actionCoords) { + runCatching { + actionCoords.buildVersionArtifacts( + types ?: actionCoords.typesUuid?.let { "" }, + metadata, + )!! + } + }.getOrNull() } } diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/InternalRoutes.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/InternalRoutes.kt index 36147ad5c7..e868db0833 100644 --- a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/InternalRoutes.kt +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/InternalRoutes.kt @@ -7,10 +7,10 @@ import io.micrometer.prometheusmetrics.PrometheusMeterRegistry fun Routing.internalRoutes(prometheusRegistry: PrometheusMeterRegistry) { get("/metrics") { - call.respondText(prometheusRegistry.scrape()) + call.respondText(text = prometheusRegistry.scrape()) } get("/status") { - call.respondText("OK") + call.respondText(text = "OK") } } diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt index 99370249b3..5c5b7c2d58 100644 --- a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt @@ -51,4 +51,4 @@ fun main() { val deliverOnRefreshRoute = System.getenv("GWKT_DELIVER_ON_REFRESH").toBoolean() -suspend fun ApplicationCall.respondNotFound() = respondText("Not found", status = HttpStatusCode.NotFound) +suspend fun ApplicationCall.respondNotFound() = respondText(text = "Not found", status = HttpStatusCode.NotFound) diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/MetadataRoutes.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/MetadataRoutes.kt index 040c36a172..631ee5f7f9 100644 --- a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/MetadataRoutes.kt +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/MetadataRoutes.kt @@ -29,7 +29,7 @@ private fun Route.metadata(refresh: Boolean = false) { val bindingArtifacts = actionCoords.buildPackageArtifacts(githubToken = getGithubToken()) if (file in bindingArtifacts) { when (val artifact = bindingArtifacts[file]) { - is String -> call.respondText(artifact) + is String -> call.respondText(text = artifact) else -> call.respondText(text = "Not found", status = HttpStatusCode.NotFound) } } else { diff --git a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/JarBuilding.kt b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/JarBuilding.kt index 0c7a3fe618..36a8ee5f91 100644 --- a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/JarBuilding.kt +++ b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/JarBuilding.kt @@ -26,9 +26,12 @@ internal data class Jars( val sourcesJar: () -> ByteArray, ) -internal fun ActionCoords.buildJars(): Jars? { +internal fun ActionCoords.buildJars( + types: String?, + metadata: String?, +): Jars? { val binding = - generateBinding(metadataRevision = NewestForVersion).also { + generateBinding(metadataRevision = NewestForVersion, types = types, explicitMetadata = metadata).also { if (it.isEmpty()) return null } diff --git a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt index 7c8306b85e..bfe5fd7205 100644 --- a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt +++ b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt @@ -20,7 +20,7 @@ internal suspend fun ActionCoords.buildMavenMetadataFile( return """ - $owner + $owner${typesUuid?.let { "__types__$it" } ?: ""} $mavenName $newest diff --git a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/ModuleBuilding.kt b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/ModuleBuilding.kt index 76dbbda419..85fff8ff28 100644 --- a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/ModuleBuilding.kt +++ b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/ModuleBuilding.kt @@ -7,7 +7,7 @@ internal fun ActionCoords.buildModuleFile() = { "formatVersion": "1.1", "component": { - "group": "$owner", + "group": "$owner${typesUuid?.let { "__types__$it" } ?: ""}", "module": "$mavenName", "version": "$version", "attributes": { diff --git a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/PomBuilding.kt b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/PomBuilding.kt index cf7f0e4205..30baec8a10 100644 --- a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/PomBuilding.kt +++ b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/PomBuilding.kt @@ -11,7 +11,7 @@ internal fun ActionCoords.buildPomFile() = 4.0.0 - $owner + $owner${typesUuid?.let { "__types__$it" } ?: ""} $mavenName $version $fullName diff --git a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/VersionArtifactsBuilding.kt b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/VersionArtifactsBuilding.kt index aef17b35be..024f201666 100644 --- a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/VersionArtifactsBuilding.kt +++ b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/VersionArtifactsBuilding.kt @@ -12,8 +12,11 @@ data class JarArtifact( val data: () -> ByteArray, ) : Artifact -fun ActionCoords.buildVersionArtifacts(): Map? { - val jars = buildJars() ?: return null +fun ActionCoords.buildVersionArtifacts( + types: String? = null, + metadata: String? = null, +): Map? { + val jars = buildJars(types = types, metadata = metadata) ?: return null val pom = buildPomFile() val module = buildModuleFile() return mapOf(