From aef49b1e0cd80b11c8cb989914dcb87708143cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Kautler?= Date: Mon, 27 Jan 2025 14:03:32 +0100 Subject: [PATCH] Also make it possible to supply the action manifest --- .../api/action-binding-generator.api | 8 +- .../generation/Generation.kt | 3 +- .../metadata/MetadataReading.kt | 5 + docs/user-guide/using-actions.md | 8 +- .../jitbindingserver/ArtifactRoutes.kt | 110 ++++++++++++++++-- .../workflows/mavenbinding/JarBuilding.kt | 7 +- .../mavenbinding/VersionArtifactsBuilding.kt | 7 +- 7 files changed, 131 insertions(+), 17 deletions(-) diff --git a/action-binding-generator/api/action-binding-generator.api b/action-binding-generator/api/action-binding-generator.api index d9c96a4176..e46c55c147 100644 --- a/action-binding-generator/api/action-binding-generator.api +++ b/action-binding-generator/api/action-binding-generator.api @@ -87,8 +87,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/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;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 { @@ -161,8 +161,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/generation/Generation.kt b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/generation/Generation.kt index f48580eda2..d502b2589e 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 @@ -65,8 +65,9 @@ public fun ActionCoords.generateBinding( 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, typingActualSource) = 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/docs/user-guide/using-actions.md b/docs/user-guide/using-actions.md index 3f8a59dfa3..881b35568e 100644 --- a/docs/user-guide/using-actions.md +++ b/docs/user-guide/using-actions.md @@ -78,7 +78,7 @@ While developing a typing manifest it might be a good idea to test the result wi 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 --data-binary @action-types.yml https://bindings.krzeminski.it/pbrisbin/setup-tool-action/v2/setup-tool-action-v2.pom +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 @@ -86,6 +86,12 @@ server and locally as long as you do not delete it from your local Maven reposit 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/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 38e338bb5a..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,9 +12,12 @@ 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 @@ -24,13 +27,20 @@ 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> @@ -109,17 +119,90 @@ private fun Route.postArtifact(prometheusRegistry: PrometheusMeterRegistry) { val owner = "${call.parameters["owner"]}__types__${randomUUID()}" val name = call.parameters["name"]!! val version = call.parameters["version"]!! - val types = call.receiveText() + + 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 typings:\n${it.stackTraceToString()}", + text = "Exception while parsing supplied $TYPES_PARAMETER:\n${it.stackTraceToString()}", status = HttpStatusCode.UnprocessableEntity, ) return@post } - call.toBindingArtifacts(refresh = true, owner = owner, types = types) + + call.toBindingArtifacts(refresh = true, owner = owner, types = types, metadata = metadata) call.respondText(text = "$owner:$name:$version") incrementArtifactCounter(prometheusRegistry, call) @@ -130,16 +213,29 @@ 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(types ?: actionCoords.typesUuid?.let { "" }).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(types ?: actionCoords.typesUuid?.let { "" })!! } }.getOrNull() + bindingsCache + .get(actionCoords) { + runCatching { + actionCoords.buildVersionArtifacts( + types ?: actionCoords.typesUuid?.let { "" }, + metadata, + )!! + } + }.getOrNull() } } 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 2a1e525dbe..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(types: String?): Jars? { +internal fun ActionCoords.buildJars( + types: String?, + metadata: String?, +): Jars? { val binding = - generateBinding(metadataRevision = NewestForVersion, types = types).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/VersionArtifactsBuilding.kt b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/VersionArtifactsBuilding.kt index d0c3f80e30..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(types: String? = null): Map? { - val jars = buildJars(types = types) ?: 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(