Skip to content

Commit

Permalink
Also make it possible to supply the action manifest
Browse files Browse the repository at this point in the history
  • Loading branch information
Vampire committed Feb 2, 2025
1 parent 0a9ff15 commit d391326
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ public fun ActionCoords.generateBinding(
metadata: Metadata? = null,
inputTypings: Pair<Map<String, Typing>, TypingActualSource?>? = null,
types: String? = null,
explicitMetadata: String? = null,
): List<ActionBinding> {
val metadataResolved = metadata ?: this.fetchMetadata(metadataRevision) ?: return emptyList()
val metadataResolved = metadata ?: this.fetchMetadata(metadataRevision, explicitMetadata) ?: return emptyList()
val metadataProcessed = metadataResolved.removeDeprecatedInputsIfNameClash()

val (inputTypingsResolved, typingActualSource) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion docs/user-guide/using-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,20 @@ 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
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
/*
Expand Down Expand Up @@ -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")
}
}
Expand All @@ -198,6 +279,7 @@ private suspend fun ApplicationCall.toBindingArtifacts(
refresh: Boolean,
owner: String = parameters["owner"]!!,
types: String? = null,
metadata: String? = null,
): Map<String, Artifact>? {
val nameAndPath = parameters["name"]!!.split("__")
val name = nameAndPath.first()
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ data class JarArtifact(
val data: () -> ByteArray,
) : Artifact

fun ActionCoords.buildVersionArtifacts(types: String? = null): Map<String, Artifact>? {
val jars = buildJars(types = types) ?: return null
fun ActionCoords.buildVersionArtifacts(
types: String? = null,
metadata: String? = null,
): Map<String, Artifact>? {
val jars = buildJars(types = types, metadata = metadata) ?: return null
val pom = buildPomFile()
val module = buildModuleFile()
return mapOf(
Expand Down

0 comments on commit d391326

Please sign in to comment.