From d391326ea98a6e010b4a35c90ed3239bd05706c1 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 --- .../generation/Generation.kt | 3 +- .../metadata/MetadataReading.kt | 5 + docs/user-guide/using-actions.md | 8 +- .../workflows/jitbindingserver/Main.kt | 108 ++++++++++++++++-- .../workflows/mavenbinding/JarBuilding.kt | 7 +- .../mavenbinding/VersionArtifactsBuilding.kt | 7 +- 6 files changed, 124 insertions(+), 14 deletions(-) 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 930db00c32..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 @@ -62,8 +62,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 374932c769..a5c4b2f8ff 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 @@ -47,8 +47,13 @@ internal val ActionCoords.gitHubUrl: String get() = "https://github.com/$owner/$ 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 b203ed3211..ee45694e2c 100644 --- a/docs/user-guide/using-actions.md +++ b/docs/user-guide/using-actions.md @@ -39,7 +39,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 @@ -47,6 +47,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/Main.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt index b75a7bd354..79dca6ee3a 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 @@ -13,6 +13,8 @@ import io.github.typesafegithub.workflows.shared.internal.getGithubToken import io.ktor.http.ContentType import io.ktor.http.HttpHeaders.XRequestId import io.ktor.http.HttpStatusCode +import io.ktor.http.content.PartData +import io.ktor.http.content.asFlow import io.ktor.server.application.ApplicationCall import io.ktor.server.application.install import io.ktor.server.engine.embeddedServer @@ -21,6 +23,7 @@ import io.ktor.server.plugins.callid.CallId import io.ktor.server.plugins.callid.callIdMdc import io.ktor.server.plugins.callid.generate import io.ktor.server.plugins.calllogging.CallLogging +import io.ktor.server.request.receiveMultipart import io.ktor.server.request.receiveText import io.ktor.server.response.respondBytes import io.ktor.server.response.respondText @@ -30,11 +33,18 @@ import io.ktor.server.routing.head import io.ktor.server.routing.post import io.ktor.server.routing.route import io.ktor.server.routing.routing +import io.ktor.utils.io.readRemaining +import io.ktor.utils.io.readText import io.opentelemetry.instrumentation.ktor.v3_0.server.KtorServerTracing 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 = System /* @@ -178,17 +188,88 @@ private fun Route.artifact( 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 + when (it) { + is PartData.FileItem -> Result.success(it.provider().readRemaining().readText()) + is PartData.FormItem -> Result.success(it.value) + else -> { + logger.error { "Unexpected part data ${it::class.simpleName}" } + Result.failure() + } + } + }.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(bindingsCache, refresh = true, owner = owner, types = types) + + call.toBindingArtifacts(bindingsCache, refresh = true, owner = owner, types = types, metadata = metadata) call.respondText(text = "$owner:$name:$version") } } @@ -198,6 +279,7 @@ private suspend fun ApplicationCall.toBindingArtifacts( refresh: Boolean, owner: String = parameters["owner"]!!, types: String? = null, + metadata: String? = null, ): Map? { val nameAndPath = parameters["name"]!!.split("__") val name = nameAndPath.first() @@ -223,13 +305,23 @@ private suspend fun ApplicationCall.toBindingArtifacts( logger.info { "➡️ Requesting ${actionCoords.prettyPrint}" } val bindingArtifacts = if (refresh) { - actionCoords.buildVersionArtifacts(types ?: typesUuid?.let { "" }).also { - bindingsCache.put(actionCoords, Result.of(it)) - } + actionCoords + .buildVersionArtifacts( + types ?: typesUuid?.let { "" }, + metadata, + ).also { + bindingsCache.put(actionCoords, Result.of(it)) + } } else { bindingsCache - .get(actionCoords) { Result.of(actionCoords.buildVersionArtifacts(types ?: typesUuid?.let { "" })) } - .getOrNull() + .get(actionCoords) { + Result.of( + actionCoords.buildVersionArtifacts( + types ?: typesUuid?.let { "" }, + metadata, + ), + ) + }.getOrNull() } return bindingArtifacts } 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(