├── .github ├── CODEOWNERS └── actions │ └── test-action │ └── action.yml ├── version.txt ├── test-action ├── webpack.config.d │ └── github.action.config.js ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradle.properties ├── README.md ├── settings.gradle.kts ├── build.gradle.kts ├── src │ └── main │ │ └── kotlin │ │ └── Main.kt └── gradlew.bat ├── kotlin-js-action ├── kotlin-js-action │ ├── gradle.properties │ ├── src │ │ ├── main │ │ │ └── kotlin │ │ │ │ ├── internal │ │ │ │ ├── core │ │ │ │ │ ├── Typealiases.kt │ │ │ │ │ ├── summary.module_@actions_core.kt │ │ │ │ │ └── core.module_@actions_core.kt │ │ │ │ ├── httpclient │ │ │ │ │ ├── Typealiases.kt │ │ │ │ │ ├── interfaces.module_@actions_http-client.kt │ │ │ │ │ └── index.module_@actions_http-client.kt │ │ │ │ ├── Typealiases.kt │ │ │ │ ├── glob │ │ │ │ │ ├── internal-hash-file-options.module_@actions_glob.kt │ │ │ │ │ ├── internal-globber.module_@actions_glob.kt │ │ │ │ │ ├── glob.module_@actions_glob.kt │ │ │ │ │ └── internal-glob-options.module_@actions_glob.kt │ │ │ │ ├── github │ │ │ │ │ ├── github.module_@actions_github.kt │ │ │ │ │ ├── Typealiases.kt │ │ │ │ │ ├── context.module_@actions_github.kt │ │ │ │ │ ├── RequestRequestOptions.module_@octokit_types.kt │ │ │ │ │ ├── types.module_@octokit_core.kt │ │ │ │ │ ├── index.module_@octokit_core.kt │ │ │ │ │ └── interfaces.module_@actions_github.kt │ │ │ │ ├── artifact │ │ │ │ │ ├── download-options.module_@actions_artifact.kt │ │ │ │ │ ├── upload-options.module_@actions_artifact.kt │ │ │ │ │ └── artifact-client.module_@actions_artifact.kt │ │ │ │ ├── exec │ │ │ │ │ ├── exec.module_@actions_exec.kt │ │ │ │ │ └── interfaces.module_@actions_exec.kt │ │ │ │ ├── cache │ │ │ │ │ ├── cache.module_@actions_cache.kt │ │ │ │ │ └── options.module_@actions_cache.kt │ │ │ │ ├── lib.es2018.asyncgenerator.module_dukat.kt │ │ │ │ ├── index.module_before-after-hook.kt │ │ │ │ ├── lib.es2018.asynciterable.module_dukat.kt │ │ │ │ ├── io │ │ │ │ │ └── io.module_@actions_io.kt │ │ │ │ └── toolcache │ │ │ │ │ └── toolcache.kt │ │ │ │ └── com │ │ │ │ └── rnett │ │ │ │ └── action │ │ │ │ ├── core │ │ │ │ ├── PATH.kt │ │ │ │ ├── SummaryTableItem.kt │ │ │ │ ├── outputs.kt │ │ │ │ ├── AnnotationProperties.kt │ │ │ │ ├── inputs.kt │ │ │ │ ├── state.kt │ │ │ │ ├── TopLevel.kt │ │ │ │ ├── logger.kt │ │ │ │ └── Env.kt │ │ │ │ ├── exec │ │ │ │ ├── ExecResult.kt │ │ │ │ └── Shell.kt │ │ │ │ ├── httpclient │ │ │ │ ├── Auth.kt │ │ │ │ ├── HttpResponse.kt │ │ │ │ ├── RequestHandler.kt │ │ │ │ └── Headers.kt │ │ │ │ ├── github │ │ │ │ └── context.kt │ │ │ │ ├── artifact │ │ │ │ ├── Responses.kt │ │ │ │ └── artifact.kt │ │ │ │ ├── OperatingSystem.kt │ │ │ │ ├── Utils.kt │ │ │ │ └── io │ │ │ │ └── io.kt │ │ └── test │ │ │ ├── resources │ │ │ └── archives │ │ │ │ ├── test.7z │ │ │ │ ├── test.zip │ │ │ │ ├── test.tar.bz2 │ │ │ │ ├── test.tar.gz │ │ │ │ └── test.tar │ │ │ └── kotlin │ │ │ └── com │ │ │ └── rnett │ │ │ └── action │ │ │ ├── TestOS.kt │ │ │ ├── TestEnv.kt │ │ │ ├── TestStreamUtils.kt │ │ │ ├── TestWithDir.kt │ │ │ ├── TestExec.kt │ │ │ ├── TestToolCache.kt │ │ │ └── TestGlob.kt │ ├── README.md │ ├── packages.md │ └── build.gradle.kts ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradle.properties ├── build-logic │ ├── common-module │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ └── kotlin │ │ │ └── kjs-action.common-module.gradle.kts │ ├── js-module │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ └── kotlin │ │ │ └── kjs-action.js-module.gradle.kts │ ├── publishing │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ └── kotlin │ │ │ └── kjs-action.publishing.gradle.kts │ ├── metadata │ │ ├── src │ │ │ └── main │ │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── rnett │ │ │ │ └── action │ │ │ │ ├── MetadataExtension.kt │ │ │ │ └── MetadataPlugin.kt │ │ └── build.gradle.kts │ ├── docs │ │ ├── src │ │ │ └── main │ │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── rnett │ │ │ │ └── action │ │ │ │ ├── LeafDocsExtension.kt │ │ │ │ ├── RootDocsExtension.kt │ │ │ │ ├── DocsRootPlugin.kt │ │ │ │ └── DocsLeafPlugin.kt │ │ └── build.gradle.kts │ └── settings.gradle.kts ├── serialization │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ ├── test │ │ └── kotlin │ │ │ └── com │ │ │ └── rnett │ │ │ └── action │ │ │ └── serialization │ │ │ ├── TestSerialization.kt │ │ │ └── TestJsonHttpClient.kt │ │ └── main │ │ └── kotlin │ │ └── com │ │ └── rnett │ │ └── action │ │ └── serialization │ │ ├── HttpClient.kt │ │ └── ReadOnlyDelegates.kt ├── kotlin-js-action-plugin │ ├── README.md │ ├── src │ │ └── main │ │ │ └── kotlin │ │ │ └── com │ │ │ └── rnett │ │ │ └── action │ │ │ ├── Constants.kt │ │ │ ├── GithubActionPlugin.kt │ │ │ ├── KotlinAction.kt │ │ │ └── AutoBuildWorkflow.kt │ └── build.gradle.kts ├── DOCS.md ├── build.gradle.kts ├── settings.gradle.kts └── gradlew.bat ├── .idea ├── vcs.xml ├── kotlinc.xml ├── misc.xml ├── artifacts │ ├── test_action_1_3_0_SNAPSHOT.xml │ ├── serialization_1_5_1_SNAPSHOT.xml │ ├── kotlin_js_action_1_5_1_SNAPSHOT.xml │ ├── serialization_1_6_0_SNAPSHOT.xml │ └── kotlin_js_action_1_6_0_SNAPSHOT.xml ├── kotlin-js-action.iml ├── modules │ ├── 31940085 │ │ └── kotlin-js-action.build-logic.docs.iml │ ├── 904965605 │ │ └── com.github.rnett.ktjs-github-action.kotlin-js-action-parent.iml │ ├── 1473984502 │ │ └── kotlin-js-action.build-logic.js-module.iml │ ├── 1664358933 │ │ └── kotlin-js-action.build-logic.iml │ ├── 1757210950 │ │ └── com.github.rnett.ktjs-github-action.kotlin-js-action-parent.serialization.iml │ ├── test-action.iml │ ├── -1802679831 │ │ └── kotlin-js-action.build-logic.metadata.iml │ ├── -921324371 │ │ └── kotlin-js-action.build-logic.publishing.iml │ ├── -868266476 │ │ └── kotlin-js-action.build-logic.common-module.iml │ ├── -224497576 │ │ └── com.github.rnett.ktjs-github-action.kotlin-js-action-parent.kotlin-js-action.iml │ └── -898967992 │ │ └── com.github.rnett.ktjs-github-action.kotlin-js-action-parent.kotlin-js-action-plugin.iml ├── jarRepositories.xml └── compiler.xml ├── libs.versions.toml ├── .gitignore ├── CONTRIBUTING.md ├── CHANGELOG.md └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @rnett 2 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 1.6.1-SNAPSHOT 2 | -------------------------------------------------------------------------------- /test-action/webpack.config.d/github.action.config.js: -------------------------------------------------------------------------------- 1 | config.target = 'node'; -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.js.generate.executable.default=false 2 | kotlin.js.generate.externals=false -------------------------------------------------------------------------------- /test-action/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rnett/kotlin-js-action/HEAD/test-action/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /kotlin-js-action/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rnett/kotlin-js-action/HEAD/kotlin-js-action/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/core/Typealiases.kt: -------------------------------------------------------------------------------- 1 | package internal.core 2 | 3 | internal typealias SummaryTableRow = Array -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/test/resources/archives/test.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rnett/kotlin-js-action/HEAD/kotlin-js-action/kotlin-js-action/src/test/resources/archives/test.7z -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/test/resources/archives/test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rnett/kotlin-js-action/HEAD/kotlin-js-action/kotlin-js-action/src/test/resources/archives/test.zip -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/test/resources/archives/test.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rnett/kotlin-js-action/HEAD/kotlin-js-action/kotlin-js-action/src/test/resources/archives/test.tar.bz2 -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/test/resources/archives/test.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rnett/kotlin-js-action/HEAD/kotlin-js-action/kotlin-js-action/src/test/resources/archives/test.tar.gz -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/httpclient/Typealiases.kt: -------------------------------------------------------------------------------- 1 | package internal.httpclient 2 | 3 | internal typealias HttpClientError = Error 4 | 5 | internal typealias HttpError = Error -------------------------------------------------------------------------------- /test-action/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | org.gradle.parallel=false 3 | kotlin.code.style=official 4 | kotlin.js.generate.executable.default=false 5 | kotlin.js.generate.externals=false 6 | sourceLinkBranch=main -------------------------------------------------------------------------------- /test-action/README.md: -------------------------------------------------------------------------------- 1 | # test-action 2 | 3 | A test action for integration tests. See [test-action/action.yml](../.github/actions/test-action/action.yml) for the 4 | action definition and [the CI](../.github/workflows/ci.yml#L70) for usage. -------------------------------------------------------------------------------- /test-action/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /kotlin-js-action/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /kotlin-js-action/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | org.gradle.parallel=false 3 | kotlin.code.style=official 4 | kotlin.js.generate.executable.default=false 5 | kotlin.js.generate.externals=false 6 | org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=1024m 7 | sourceLinkBranch=main -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/common-module/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } 9 | 10 | dependencies { 11 | api(project(":docs")) 12 | api(project(":publishing")) 13 | } 14 | -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/js-module/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } 9 | 10 | dependencies { 11 | api(project(":common-module")) 12 | api(libs.build.kotlin.js) 13 | } 14 | -------------------------------------------------------------------------------- /kotlin-js-action/serialization/README.md: -------------------------------------------------------------------------------- 1 | # Module Kotlin JS GitHub Action SDK Serialization support 2 | 3 | Adds support for Kotlinx serialization, by adding delegate transformers. 4 | 5 | Example: 6 | 7 | ```kotlin 8 | val json = Json{} 9 | var testState: MyData by state.deserialized(json) 10 | ``` -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action-plugin/README.md: -------------------------------------------------------------------------------- 1 | # Module Kotlin JS GitHub Action Gradle Plugin 2 | 3 | A gradle plugin (`com.github.rnett.ktjs-github-action`) that provides functions to configure Kotlin/JS for GitHub 4 | actions, and to auto-generate a GitHub Actions workflow to update action distributables on push. -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/publishing/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } 9 | 10 | dependencies { 11 | api(project(":metadata")) 12 | implementation(project(":docs")) 13 | implementation(libs.build.publish) 14 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action-plugin/src/main/kotlin/com/rnett/action/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | internal object Constants { 4 | val taskGroup = "kotlin js github action" 5 | val createWebpackTaskName = "createGithubActionWebpackConfig" 6 | val generateWorkflowTaskName = "generateBuildWorkflow" 7 | 8 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/test/kotlin/com/rnett/action/TestOS.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class TestOS { 7 | @Test 8 | fun current() { 9 | assertEquals(TestEnv.os.lowercase(), OperatingSystem.current.name.lowercase()) 10 | } 11 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action-plugin/src/main/kotlin/com/rnett/action/GithubActionPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | 6 | /** 7 | * Does nothing, just a placeholder. 8 | */ 9 | class GithubActionPlugin : Plugin { 10 | override fun apply(target: Project) { 11 | 12 | } 13 | } -------------------------------------------------------------------------------- /kotlin-js-action/DOCS.md: -------------------------------------------------------------------------------- 1 | # Kotlin JS GitHub Action SDK 2 | 3 | See [the README](https://github.com/rnett/kotlin-js-action#readme) for an overview. 4 | 5 | Artifacts: 6 | 7 | * `kotlin-js-action` - the GitHub Actions SDK. 8 | * `kotlin-js-action-plugin` - the Gradle plugin to help with build setup. 9 | * `serialization` - [Kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) helpers for the SDK. -------------------------------------------------------------------------------- /.idea/artifacts/test_action_1_3_0_SNAPSHOT.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/test-action/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /kotlin-js-action/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kjs-action.docs-root") 3 | alias(libs.plugins.kotlin.js) apply false 4 | alias(libs.plugins.publish) apply false 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | gradlePluginPortal() 10 | } 11 | 12 | metadata { 13 | title.set("Kotlin/JS GitHub Actions SDK") 14 | } 15 | 16 | docs { 17 | readmeHeader.set("Kotlin JS GitHub Action SDK") 18 | } -------------------------------------------------------------------------------- /.idea/artifacts/serialization_1_5_1_SNAPSHOT.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/kotlin-js-action/serialization/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/kotlin_js_action_1_5_1_SNAPSHOT.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/kotlin-js-action/kotlin-js-action/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/serialization_1_6_0_SNAPSHOT.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/kotlin-js-action/serialization/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/kotlin_js_action_1_6_0_SNAPSHOT.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/kotlin-js-action/kotlin-js-action/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/Typealiases.kt: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | 4 | 5 | internal typealias HookMethod = (options: O) -> dynamic 6 | 7 | internal typealias BeforeHook = (options: O) -> Unit 8 | 9 | internal typealias ErrorHook = (error: E, options: O) -> Unit 10 | 11 | internal typealias AfterHook = (result: R, options: O) -> Unit 12 | 13 | internal typealias WrapHook = (hookMethod: HookMethod, options: O) -> dynamic -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/glob/internal-hash-file-options.module_@actions_glob.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | 8 | package internal.glob 9 | 10 | internal external interface HashFileOptions { 11 | var followSymbolicLinks: Boolean? 12 | get() = definedExternally 13 | set(value) = definedExternally 14 | } -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/metadata/src/main/kotlin/com/rnett/action/MetadataExtension.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.provider.Property 5 | import org.gradle.api.provider.Provider 6 | import javax.inject.Inject 7 | 8 | abstract class MetadataExtension @Inject constructor(project: Project) { 9 | abstract val title: Property 10 | 11 | val description: Provider = project.provider { project.description } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/metadata/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } 9 | 10 | tasks.processResources { 11 | from(rootDir.parentFile.parentFile.resolve("version.txt")) 12 | } 13 | 14 | gradlePlugin { 15 | plugins { 16 | create("kjs-metadata") { 17 | id = "kjs-action.metadata" 18 | implementationClass = "com.rnett.action.MetadataPlugin" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/github/github.module_@actions_github.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | @file:JsModule("@actions/github") 3 | @file:JsNonModule 4 | 5 | package internal.github 6 | 7 | import kotlin.js.* 8 | 9 | internal external var context: Context 10 | 11 | internal external fun getOctokit(token: String, options: OctokitOptions = definedExternally): InstanceType -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/artifact/download-options.module_@actions_artifact.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | @file:JsModule("@actions/artifact") 3 | @file:JsNonModule 4 | 5 | package internal.artifact 6 | 7 | import kotlin.js.* 8 | 9 | internal external interface DownloadOptions { 10 | var createArtifactFolder: Boolean? 11 | get() = definedExternally 12 | set(value) = definedExternally 13 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/README.md: -------------------------------------------------------------------------------- 1 | # Module Kotlin JS GitHub Action SDK 2 | 3 | Kotlin JS utilities for writing GitHub Actions, including wrappers 4 | for [actions/toolkit](https://github.com/actions/toolkit) packages, except for `@actions/github` 5 | and `@actions/tool-cache`. `@actions/tool-cache` will be added once blocking `dukat` bugs are fixes. 6 | 7 | In addition to the `@actions` bindings, a utility `Path` class modeled after Python's `pathlib` is included, as are some 8 | miscellaneous utilities (like `JsObject` and an OS enum and detector). 9 | -------------------------------------------------------------------------------- /kotlin-js-action/serialization/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kjs-action.js-module") 3 | alias(libs.plugins.kotlinx.serialization) 4 | } 5 | 6 | description = "Support for Kotlinx serialization use with GitHub APIs" 7 | metadata{ 8 | title.set("Kotlin JS GitHub Action SDK Serialization support") 9 | } 10 | 11 | dependencies { 12 | testImplementation(kotlin("test")) 13 | testImplementation(libs.kotlinx.coroutines.test) 14 | 15 | api(libs.kotlinx.serialization.json) 16 | implementation(project(":kotlin-js-action")) 17 | } 18 | -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/docs/src/main/kotlin/com/rnett/action/LeafDocsExtension.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import org.gradle.api.provider.Property 4 | import org.gradle.api.provider.Provider 5 | import org.gradle.api.Project 6 | import org.jetbrains.dokka.Platform 7 | import javax.inject.Inject 8 | 9 | abstract class LeafDocsExtension @Inject constructor(val project: Project) { 10 | abstract val platform: Property 11 | 12 | val title: Provider get() = project.extensions.getByType(MetadataExtension::class.java).title 13 | } -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/docs/src/main/kotlin/com/rnett/action/RootDocsExtension.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import org.gradle.api.provider.Property 4 | import org.gradle.api.provider.Provider 5 | import org.gradle.api.Project 6 | import org.jetbrains.dokka.Platform 7 | import javax.inject.Inject 8 | 9 | abstract class RootDocsExtension @Inject constructor(val project: Project) { 10 | abstract val readmeHeader: Property 11 | 12 | val title: Provider get() = project.extensions.getByType(MetadataExtension::class.java).title 13 | } -------------------------------------------------------------------------------- /.github/actions/test-action/action.yml: -------------------------------------------------------------------------------- 1 | name: "Test action" 2 | description: "Action to test Kotlin JS SDK" 3 | inputs: 4 | required-input: 5 | description: "A required input" 6 | required: true 7 | optional-no-default: 8 | description: "Optional input" 9 | required: false 10 | with-default: 11 | description: "Input with default" 12 | required: false 13 | default: "Test" 14 | multiline: 15 | description: "Multiline input" 16 | required: true 17 | runs: 18 | using: 'node16' 19 | main: '../../../test-action/build/dist/index.js' 20 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/core/PATH.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.core 2 | 3 | import com.rnett.action.Path 4 | 5 | /** 6 | * A setter for the path. 7 | */ 8 | public object PATH { 9 | /** 10 | * Add a new path [inputPath] to PATH. 11 | */ 12 | public operator fun plusAssign(inputPath: String) { 13 | core.addPath(inputPath) 14 | } 15 | 16 | /** 17 | * Add a new path [inputPath] to PATH. 18 | */ 19 | public operator fun plusAssign(inputPath: Path): Unit = plusAssign(inputPath.path) 20 | } -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/common-module/src/main/kotlin/kjs-action.common-module.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.testing.AbstractTestTask 2 | 3 | plugins { 4 | id("kjs-action.docs-leaf") 5 | id("kjs-action.publishing") 6 | } 7 | 8 | tasks.withType(AbstractTestTask::class.java).configureEach { 9 | testLogging { 10 | showExceptions = true // It is true by default. Set it just for explicitness. 11 | exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL 12 | } 13 | } 14 | 15 | repositories { 16 | mavenCentral() 17 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/glob/internal-globber.module_@actions_glob.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | @file:JsModule("@actions/glob") 3 | @file:JsNonModule 4 | 5 | package internal.glob 6 | 7 | import internal.AsyncGenerator__2 8 | import kotlin.js.* 9 | 10 | internal external interface Globber { 11 | fun getSearchPaths(): Array 12 | fun glob(): Promise> 13 | fun globGenerator(): AsyncGenerator__2 14 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/github/Typealiases.kt: -------------------------------------------------------------------------------- 1 | package internal.github 2 | 3 | 4 | 5 | internal typealias Constructor = Any 6 | 7 | internal typealias ReturnTypeOf = Any 8 | 9 | internal typealias UnionToIntersection = Any 10 | 11 | internal typealias AnyFunction = (args: Any) -> Any 12 | 13 | internal typealias OctokitPlugin = (octokit: Octokit, options: OctokitOptions) -> dynamic 14 | internal typealias Signal = Any 15 | 16 | internal typealias ReturnType = Any 17 | 18 | internal typealias InstanceType = Any 19 | 20 | internal typealias Fetch = Any -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/test/kotlin/com/rnett/action/TestEnv.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import kotlinx.js.get 4 | import kotlin.reflect.KProperty 5 | 6 | object TestEnv { 7 | private operator fun getValue(thisRef: Any?, property: KProperty<*>): String { 8 | val key = "TEST_ENV_${property.name}" 9 | return currentProcess.env[key] ?: error("Test environment variable $key not set") 10 | } 11 | 12 | val os by this 13 | val projectDirPath by this 14 | val tempDir by this 15 | val userHome by this 16 | val projectDir get() = Path(projectDirPath) 17 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/exec/ExecResult.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.exec 2 | 3 | public data class ExecFailureException(val command: String, val returnCode: Int, val stderr: String): RuntimeException("Command failed with return code $returnCode and stderr: $stderr") 4 | 5 | public data class ExecResult(private val command: String, val returnCode: Int, val stdout: String, val stderr: String){ 6 | public fun throwIfFailure(): ExecResult = apply { 7 | if(returnCode != 0) 8 | throw ExecFailureException(command, returnCode, stderr) 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /test-action/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("VERSION_CATALOGS") 2 | 3 | pluginManagement { 4 | repositories { 5 | mavenCentral() 6 | gradlePluginPortal() 7 | maven("https://oss.sonatype.org/content/repositories/snapshots") { 8 | mavenContent { snapshotsOnly() } 9 | } 10 | } 11 | } 12 | 13 | dependencyResolutionManagement { 14 | versionCatalogs { 15 | create(defaultLibrariesExtensionName.get()) { 16 | from(files("../libs.versions.toml")) 17 | } 18 | } 19 | } 20 | 21 | rootProject.name = "test-action" 22 | 23 | includeBuild("../kotlin-js-action") 24 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/artifact/upload-options.module_@actions_artifact.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | @file:JsModule("@actions/artifact") 3 | @file:JsNonModule 4 | 5 | package internal.artifact 6 | 7 | import kotlin.js.* 8 | 9 | internal external interface UploadOptions { 10 | var continueOnError: Boolean? 11 | get() = definedExternally 12 | set(value) = definedExternally 13 | var retentionDays: Number? 14 | get() = definedExternally 15 | set(value) = definedExternally 16 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/glob/glob.module_@actions_glob.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | @file:JsModule("@actions/glob") 8 | @file:JsNonModule 9 | 10 | package internal.glob 11 | 12 | import kotlin.js.Promise 13 | 14 | internal external fun create(patterns: String, options: GlobOptions = definedExternally): Promise 15 | 16 | internal external fun hashFiles(patterns: String, options: HashFileOptions = definedExternally, verbose: Boolean = definedExternally): Promise -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/metadata/src/main/kotlin/com/rnett/action/MetadataPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | import org.gradle.kotlin.dsl.create 6 | 7 | class MetadataPlugin : Plugin { 8 | override fun apply(project: Project): Unit = with(project) { 9 | val extension = project.extensions.create("metadata", MetadataExtension::class) 10 | group = "com.github.rnett.ktjs-github-action" 11 | version = MetadataPlugin::class.java.getResource("/version.txt")?.readText()?.trim() ?: throw IllegalStateException("version.txt not found") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test-action/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.rnett.action.githubAction 2 | 3 | plugins { 4 | alias(libs.plugins.kotlin.js) 5 | id("com.github.rnett.ktjs-github-action") 6 | } 7 | 8 | group = "com.github.rnett.ktjs-github-action.test" 9 | version = "1.3.0-SNAPSHOT" 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation("com.github.rnett.ktjs-github-action:kotlin-js-action") 17 | implementation("com.github.rnett.ktjs-github-action:serialization") 18 | implementation(kotlin("test")) 19 | } 20 | 21 | kotlin { 22 | js(IR) { 23 | githubAction(layout.buildDirectory.dir("dist").get()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/docs/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } 9 | 10 | dependencies { 11 | api(project(":metadata")) 12 | api(libs.build.dokka) 13 | api(libs.build.dokka.versioning) 14 | } 15 | 16 | gradlePlugin { 17 | plugins { 18 | create("kjs-docs-leaf") { 19 | id = "kjs-action.docs-leaf" 20 | implementationClass = "com.rnett.action.DocsLeafPlugin" 21 | } 22 | create("kjs-docs-root") { 23 | id = "kjs-action.docs-root" 24 | implementationClass = "com.rnett.action.DocsRootPlugin" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("VERSION_CATALOGS") 2 | 3 | pluginManagement { 4 | repositories { 5 | mavenCentral() 6 | gradlePluginPortal() 7 | maven("https://oss.sonatype.org/content/repositories/snapshots") { 8 | mavenContent { snapshotsOnly() } 9 | } 10 | } 11 | } 12 | 13 | dependencyResolutionManagement { 14 | versionCatalogs { 15 | create(defaultLibrariesExtensionName.get()) { 16 | from(files("../../libs.versions.toml")) 17 | } 18 | } 19 | } 20 | 21 | include( 22 | "metadata", 23 | "docs", 24 | "common-module", 25 | "js-module", 26 | "publishing" 27 | ) 28 | -------------------------------------------------------------------------------- /kotlin-js-action/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("VERSION_CATALOGS") 2 | 3 | pluginManagement { 4 | includeBuild("build-logic") 5 | repositories { 6 | mavenCentral() 7 | gradlePluginPortal() 8 | maven("https://oss.sonatype.org/content/repositories/snapshots") { 9 | mavenContent { snapshotsOnly() } 10 | } 11 | } 12 | } 13 | 14 | dependencyResolutionManagement { 15 | versionCatalogs { 16 | create(defaultLibrariesExtensionName.get()) { 17 | from(files("../libs.versions.toml")) 18 | } 19 | } 20 | } 21 | 22 | rootProject.name = "kotlin-js-action-parent" 23 | 24 | include("kotlin-js-action", "serialization", "kotlin-js-action-plugin") 25 | -------------------------------------------------------------------------------- /.idea/kotlin-js-action.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/exec/exec.module_@actions_exec.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | @file:JsModule("@actions/exec") 8 | @file:JsNonModule 9 | 10 | package internal.exec 11 | 12 | import kotlin.js.Promise 13 | 14 | internal external fun exec( 15 | commandLine: String, 16 | args: Array = definedExternally, 17 | options: ExecOptions = definedExternally 18 | ): Promise 19 | 20 | internal external fun getExecOutput( 21 | commandLine: String, 22 | args: Array = definedExternally, 23 | options: ExecOptions = definedExternally 24 | ): Promise -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/cache/cache.module_@actions_cache.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | @file:JsModule("@actions/cache") 3 | @file:JsNonModule 4 | 5 | package internal.cache 6 | 7 | import kotlin.js.* 8 | 9 | 10 | internal external fun restoreCache( 11 | paths: Array, 12 | primaryKey: String, 13 | restoreKeys: Array = definedExternally, 14 | options: DownloadOptions = definedExternally 15 | ): Promise 16 | 17 | internal external fun saveCache(paths: Array, key: String, options: UploadOptions = definedExternally): Promise 18 | 19 | internal external fun isFeatureAvailable(): Boolean -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/github/context.module_@actions_github.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | @file:JsModule("@actions/github") 8 | @file:JsNonModule 9 | 10 | package internal.github 11 | 12 | internal external open class Context { 13 | open var payload: WebhookPayload 14 | open var eventName: String 15 | open var sha: String 16 | open var ref: String 17 | open var workflow: String 18 | open var action: String 19 | open var actor: String 20 | open var job: String 21 | open var runNumber: Number 22 | open var runId: Number 23 | open var apiUrl: String 24 | open var serverUrl: String 25 | open var graphqlUrl: String 26 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/core/SummaryTableItem.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.core 2 | 3 | public sealed interface SummaryTableItem 4 | 5 | /** 6 | * A raw text cell. 7 | */ 8 | public value class SummaryTableTextCell(public val text: String) : SummaryTableItem 9 | 10 | /** 11 | * A table cell element. 12 | * Corresponds to a ``, or a `` is [header] is `true`. 13 | * 14 | * [colspan] and [rowspan] correspond to their respective HTML attributes. 15 | */ 16 | public data class SummaryTableCell(val data: String, val header: Boolean = false, val colspan: String = "1", val rowspan: String = "1") : 17 | SummaryTableItem { 18 | public constructor(data: String, header: Boolean = false, colspan: Int, rowspan: Int = 1) : this( 19 | data, 20 | header, 21 | colspan.toString(), 22 | rowspan.toString() 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/glob/internal-glob-options.module_@actions_glob.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | @file:JsModule("@actions/glob") 8 | @file:JsNonModule 9 | 10 | package internal.glob 11 | 12 | internal external interface GlobOptions { 13 | var followSymbolicLinks: Boolean? 14 | get() = definedExternally 15 | set(value) = definedExternally 16 | var implicitDescendants: Boolean? 17 | get() = definedExternally 18 | set(value) = definedExternally 19 | var matchDirectories: Boolean? 20 | get() = definedExternally 21 | set(value) = definedExternally 22 | var omitBrokenSymbolicLinks: Boolean? 23 | get() = definedExternally 24 | set(value) = definedExternally 25 | } -------------------------------------------------------------------------------- /.idea/modules/test-action.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules/1664358933/kotlin-js-action.build-logic.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules/904965605/com.github.rnett.ktjs-github-action.kotlin-js-action-parent.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/artifact/artifact-client.module_@actions_artifact.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | @file:JsModule("@actions/artifact") 3 | @file:JsNonModule 4 | 5 | package internal.artifact 6 | 7 | import com.rnett.action.artifact.DownloadResponse 8 | import com.rnett.action.artifact.UploadResponse 9 | import kotlin.js.Promise 10 | 11 | internal external interface ArtifactClient { 12 | fun uploadArtifact(name: String, files: Array, rootDirectory: String, options: UploadOptions = definedExternally): Promise 13 | fun downloadArtifact(name: String, path: String = definedExternally, options: DownloadOptions = definedExternally): Promise 14 | fun downloadAllArtifacts(path: String = definedExternally): Promise> 15 | } 16 | 17 | internal external fun create(): ArtifactClient -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/js-module/src/main/kotlin/kjs-action.js-module.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.js") 3 | id("kjs-action.common-module") 4 | } 5 | 6 | kotlin { 7 | js(IR) { 8 | useCommonJs() 9 | nodejs { 10 | binaries.library() 11 | testTask { 12 | useMocha { 13 | timeout = "20s" 14 | } 15 | } 16 | } 17 | } 18 | explicitApi() 19 | sourceSets.configureEach { 20 | languageSettings { 21 | optIn("kotlin.contracts.ExperimentalContracts") 22 | optIn("kotlin.RequiresOptIn") 23 | } 24 | } 25 | } 26 | plugins.withType { 27 | configure { 28 | nodeVersion = "16.18.0" 29 | } 30 | } 31 | 32 | docs { 33 | platform.set(org.jetbrains.dokka.Platform.js) 34 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/lib.es2018.asyncgenerator.module_dukat.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | 3 | package internal 4 | 5 | import kotlin.js.Promise 6 | 7 | internal external interface AsyncGenerator : AsyncIterator { 8 | override fun next(vararg args: Any /* JsTuple<> | JsTuple */): Promise | IteratorReturnResult */> 9 | fun `return`(value: TReturn): Promise | IteratorReturnResult */> 10 | fun `return`(value: Promise): Promise | IteratorReturnResult */> 11 | override var `throw`: (e: Any) -> Promise | IteratorReturnResult */> 12 | } 13 | 14 | internal external interface AsyncGenerator__2 : AsyncGenerator -------------------------------------------------------------------------------- /.idea/modules/31940085/kotlin-js-action.build-logic.docs.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/github/RequestRequestOptions.module_@octokit_types.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | @file:JsModule("@octokit/types") 3 | @file:JsNonModule 4 | 5 | package internal.github 6 | 7 | import node.http.Agent 8 | import kotlin.js.* 9 | 10 | internal external interface RequestRequestOptions { 11 | var agent: Agent? 12 | get() = definedExternally 13 | set(value) = definedExternally 14 | var fetch: Fetch? 15 | get() = definedExternally 16 | set(value) = definedExternally 17 | var signal: Signal? 18 | get() = definedExternally 19 | set(value) = definedExternally 20 | var timeout: Number? 21 | get() = definedExternally 22 | set(value) = definedExternally 23 | @nativeGetter 24 | operator fun get(option: String): Any? 25 | @nativeSetter 26 | operator fun set(option: String, value: Any) 27 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/httpclient/Auth.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.httpclient 2 | 3 | import com.rnett.action.encodeBase64 4 | 5 | /** 6 | * Request handler for basic auth. 7 | */ 8 | public data class BasicAuthHandler(val username: String, val password: String) : HeaderProvider { 9 | override fun MutableHeaders.headers() { 10 | this["Authorization"] = "Basic ${"$username:$password".encodeBase64()}" 11 | } 12 | } 13 | 14 | /** 15 | * Request handler for bearer auth. 16 | */ 17 | public data class BearerAuthHandler(val token: String) : HeaderProvider { 18 | override fun MutableHeaders.headers() { 19 | this["Authorization"] = "Bearer $token" 20 | } 21 | } 22 | 23 | /** 24 | * Request handler for personal access token auth. 25 | */ 26 | public data class PersonalAccessTokenAuthHandler(val token: String) : HeaderProvider { 27 | override fun MutableHeaders.headers() { 28 | this["Authorization"] = "Basic ${"PAT:$token".encodeBase64()}" 29 | } 30 | } -------------------------------------------------------------------------------- /.idea/modules/-1802679831/kotlin-js-action.build-logic.metadata.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules/1473984502/kotlin-js-action.build-logic.js-module.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules/-921324371/kotlin-js-action.build-logic.publishing.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules/-868266476/kotlin-js-action.build-logic.common-module.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules/1757210950/com.github.rnett.ktjs-github-action.kotlin-js-action-parent.serialization.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/github/context.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.github 2 | 3 | import com.rnett.action.Path 4 | import com.rnett.action.core.env 5 | 6 | public object github { 7 | public object context { 8 | public val eventName: String get() = internal.github.context.eventName 9 | public val sha: String get() = internal.github.context.sha 10 | public val ref: String get() = internal.github.context.ref 11 | public val workflow: String get() = internal.github.context.workflow 12 | public val action: String get() = internal.github.context.action 13 | public val actor: String get() = internal.github.context.actor 14 | public val job: String get() = internal.github.context.job 15 | public val runId: Int get() = internal.github.context.runId.toInt() 16 | public val runNumber: Int get() = internal.github.context.runNumber.toInt() 17 | public val workspace: String by env.required("GITHUB_WORKSPACE") 18 | public val workspacePath: Path get() = Path(workspace) 19 | } 20 | } -------------------------------------------------------------------------------- /.idea/modules/-224497576/com.github.rnett.ktjs-github-action.kotlin-js-action-parent.kotlin-js-action.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules/-898967992/com.github.rnett.ktjs-github-action.kotlin-js-action-parent.kotlin-js-action-plugin.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/cache/options.module_@actions_cache.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | @file:JsModule("@actions/cache") 3 | @file:JsNonModule 4 | 5 | package internal.cache 6 | 7 | import kotlin.js.* 8 | 9 | internal external interface UploadOptions { 10 | var uploadConcurrency: Number? 11 | get() = definedExternally 12 | set(value) = definedExternally 13 | var uploadChunkSize: Number? 14 | get() = definedExternally 15 | set(value) = definedExternally 16 | } 17 | 18 | internal external interface DownloadOptions { 19 | var useAzureSdk: Boolean? 20 | get() = definedExternally 21 | set(value) = definedExternally 22 | var downloadConcurrency: Number? 23 | get() = definedExternally 24 | set(value) = definedExternally 25 | var timeoutInMs: Number? 26 | get() = definedExternally 27 | set(value) = definedExternally 28 | var segmentTimeoutInMs: Number? 29 | get() = definedExternally 30 | set(value) = definedExternally 31 | } -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/exec/Shell.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.exec 2 | 3 | /** 4 | * Represents a shell used to run commands. 5 | */ 6 | public abstract class Shell(public val escapeWindows: Boolean = true) { 7 | public abstract fun shellCommand(command: String): String 8 | public abstract fun args(command: String): Array 9 | 10 | public object bash : ConstantShell("bash") { 11 | override fun args(command: String): Array = arrayOf("-c", command) 12 | } 13 | 14 | public object cmd : ConstantShell("cmd") { 15 | override fun args(command: String): Array = arrayOf("/c", command) 16 | } 17 | 18 | /** 19 | * **Note that output redirects with > will be written in utf16-le with a BOM** 20 | */ 21 | public object powershell : ConstantShell("powershell") { 22 | override fun args(command: String): Array = arrayOf("-c", command) 23 | } 24 | 25 | } 26 | 27 | /** 28 | * A shell where the shell command is constant. 29 | */ 30 | public abstract class ConstantShell(public val shellCommand: String, escapeWindows: Boolean = true) : 31 | Shell(escapeWindows) { 32 | override fun shellCommand(command: String): String = shellCommand 33 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kjs-action.common-module") 3 | `kotlin-dsl` 4 | } 5 | 6 | description = "A Gradle plugin to easily configure GitHub action packing" 7 | metadata { 8 | title.set("Kotlin JS GitHub Action Gradle Plugin") 9 | } 10 | 11 | kotlin { 12 | target { 13 | attributes { 14 | attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8) 15 | } 16 | } 17 | } 18 | 19 | tasks.compileJava { 20 | sourceCompatibility = "1.8" 21 | targetCompatibility = "1.8" 22 | } 23 | 24 | tasks.compileKotlin { 25 | kotlinOptions { 26 | jvmTarget = "1.8" 27 | } 28 | } 29 | 30 | docs { 31 | platform.set(org.jetbrains.dokka.Platform.jvm) 32 | } 33 | 34 | dependencies { 35 | compileOnly(kotlin("gradle-plugin")) 36 | implementation(kotlin("gradle-plugin-api")) 37 | } 38 | 39 | gradlePlugin { 40 | plugins { 41 | create("kotlinJsGithubActionPlugin") { 42 | id = "com.github.rnett.ktjs-github-action" 43 | displayName = "Kotlin JS GitHub Action Gradle Plugin" 44 | description = "Kotlin JS GitHub Action Gradle Plugin" 45 | implementationClass = "com.rnett.action.GithubActionPlugin" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /kotlin-js-action/serialization/src/test/kotlin/com/rnett/action/serialization/TestSerialization.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.serialization 2 | 3 | import com.rnett.action.core.env 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.encodeToString 7 | import kotlinx.serialization.json.Json 8 | import kotlin.properties.ReadOnlyProperty 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | import kotlin.test.assertNull 12 | 13 | @Serializable 14 | data class TestData(val i: Int, val s: String) 15 | 16 | private val original = TestData(2, "test") 17 | 18 | @OptIn(ExperimentalSerializationApi::class) 19 | internal class BasicTest { 20 | 21 | @Test 22 | fun testBasics() { 23 | val json = Json { } 24 | val delegate = ReadOnlyProperty { _, _ -> json.encodeToString(original) } 25 | val typedDelegate: TestData by delegate.deserialize(json) 26 | 27 | assertEquals(original, typedDelegate) 28 | } 29 | 30 | @Test 31 | fun testEnv() { 32 | val json = Json { } 33 | var envData: TestData? by env.deserializeNotNull(json) 34 | envData = null 35 | assertNull(envData) 36 | envData = original 37 | assertEquals(original, envData) 38 | } 39 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/index.module_before-after-hook.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | @file:JsModule("before-after-hook") 3 | @file:JsNonModule 4 | 5 | package internal 6 | 7 | import kotlin.js.* 8 | 9 | internal external interface HookCollection { 10 | @nativeInvoke 11 | operator fun invoke(name: String, hookMethod: HookMethod, options: Any = definedExternally): Promise 12 | @nativeInvoke 13 | operator fun invoke(name: String, hookMethod: HookMethod): Promise 14 | @nativeInvoke 15 | operator fun invoke(name: Array, hookMethod: HookMethod, options: Any = definedExternally): Promise 16 | @nativeInvoke 17 | operator fun invoke(name: Array, hookMethod: HookMethod): Promise 18 | fun before(name: String, beforeHook: BeforeHook) 19 | fun error(name: String, errorHook: ErrorHook) 20 | fun after(name: String, afterHook: AfterHook) 21 | fun wrap(name: String, wrapHook: WrapHook) 22 | fun remove(name: String, hook: BeforeHook) 23 | fun remove(name: String, hook: ErrorHook) 24 | fun remove(name: String, hook: WrapHook) 25 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/packages.md: -------------------------------------------------------------------------------- 1 | # Package com.rnett.action 2 | 3 | `Path`, OS, and utilities. 4 | 5 | # Package com.rnett.action.artifact 6 | 7 | Wrappers for [@actions/artifact](https://github.com/actions/toolkit/blob/main/packages/artifact). 8 | 9 | # Package com.rnett.action.cache 10 | 11 | Wrappers for [@actions/cache](https://github.com/actions/toolkit/blob/main/packages/cache). 12 | 13 | # Package com.rnett.action.core 14 | 15 | Wrappers for [@actions/core](https://github.com/actions/toolkit/blob/main/packages/core). 16 | 17 | # Package com.rnett.action.exec 18 | 19 | Wrappers for [@actions/exec](https://github.com/actions/toolkit/blob/main/packages/exec). 20 | 21 | # Package com.rnett.action.glob 22 | 23 | Wrappers for [@actions/glob](https://github.com/actions/toolkit/blob/main/packages/glob). 24 | 25 | # Package com.rnett.action.io 26 | 27 | Wrappers for [@actions/io](https://github.com/actions/toolkit/blob/main/packages/io). 28 | 29 | # Package com.rnett.action.tool-cache 30 | 31 | Wrappers for [@actions/tool-cache](https://github.com/actions/toolkit/blob/main/packages/tool-cache). 32 | 33 | # Package com.rnett.action.httpclient 34 | 35 | Wrappers for [@actions/http-client](https://github.com/actions/http-client). 36 | 37 | # Package com.rnett.action.delegates 38 | 39 | Delegation base classes, utilities, and transforms. -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/lib.es2018.asynciterable.module_dukat.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | 3 | package internal 4 | 5 | import kotlin.js.Promise 6 | 7 | internal external interface `L$0` { 8 | @nativeInvoke 9 | operator fun invoke(value: TReturn = definedExternally): Promise | IteratorReturnResult */> 10 | 11 | @nativeInvoke 12 | operator fun invoke(): Promise | IteratorReturnResult */> 13 | 14 | @nativeInvoke 15 | operator fun invoke(value: Promise = definedExternally): Promise | IteratorReturnResult */> 16 | } 17 | 18 | internal external interface AsyncIterator { 19 | fun next(vararg args: Any /* JsTuple<> | JsTuple */): Promise | IteratorReturnResult */> 20 | val `return`: `L$0`? 21 | get() = definedExternally 22 | val `throw`: ((e: Any) -> Promise | IteratorReturnResult */>)? 23 | } 24 | 25 | internal external interface AsyncIterator__1 : AsyncIterator 26 | 27 | internal external interface AsyncIterable 28 | 29 | internal external interface AsyncIterableIterator : AsyncIterator__1 -------------------------------------------------------------------------------- /libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "1.7.21" 3 | kotlinx-serialization = "1.4.1" 4 | kotlinx-coroutines = "1.6.4" 5 | kotlin-wrappers-node = "18.11.5-pre.414" 6 | 7 | publish = "0.22.0" 8 | dokka = "1.7.20" 9 | 10 | [libraries] 11 | 12 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 13 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } 14 | 15 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } 16 | 17 | kotlin-wrappers-node = { module = "org.jetbrains.kotlin-wrappers:kotlin-node", version.ref = "kotlin-wrappers-node" } 18 | 19 | build-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "publish" } 20 | build-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } 21 | build-dokka-versioning = { module = "org.jetbrains.dokka:versioning-plugin", version.ref = "dokka" } 22 | 23 | build-kotlin-js = { module = "org.jetbrains.kotlin.js:org.jetbrains.kotlin.js.gradle.plugin", version.ref = "kotlin" } 24 | 25 | [plugins] 26 | kotlin-js = { id = "org.jetbrains.kotlin.js", version.ref = "kotlin" } 27 | 28 | kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 29 | 30 | publish = { id = "com.vanniktech.maven.publish", version.ref = "publish" } -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/publishing/src/main/kotlin/kjs-action.publishing.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.rnett.action.MetadataExtension 2 | import com.vanniktech.maven.publish.SonatypeHost 3 | 4 | plugins { 5 | id("com.vanniktech.maven.publish") 6 | } 7 | 8 | mavenPublishing { 9 | publishToMavenCentral(SonatypeHost.DEFAULT) 10 | 11 | signAllPublications() 12 | 13 | pom { 14 | name.set(project.providers.provider { project.extensions.getByType() }.flatMap { it.title }) 15 | description.set(project.providers.provider { project.description }) 16 | inceptionYear.set("2021") 17 | url.set("https://github.com/rnett/kotlin-js-action/") 18 | 19 | licenses { 20 | license { 21 | name.set("The Apache Software License, Version 2.0") 22 | url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") 23 | distribution.set("repo") 24 | } 25 | } 26 | 27 | scm { 28 | url.set("https://github.com/rnett/kotlin-js-action.git") 29 | connection.set("scm:git:git://github.com/rnett/kotlin-js-action.git") 30 | developerConnection.set("scm:git:ssh://git@github.com/rnett/kotlin-js-action.git") 31 | } 32 | 33 | developers { 34 | developer { 35 | id.set("rnett") 36 | name.set("Ryan Nett") 37 | url.set("https://github.com/rnett/") 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/io/io.module_@actions_io.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | @file:JsModule("@actions/io") 8 | @file:JsNonModule 9 | 10 | package internal.io 11 | 12 | import kotlin.js.Promise 13 | 14 | internal external interface CopyOptions { 15 | var recursive: Boolean? 16 | get() = definedExternally 17 | set(value) = definedExternally 18 | var force: Boolean? 19 | get() = definedExternally 20 | set(value) = definedExternally 21 | var copySourceDirectory: Boolean? 22 | get() = definedExternally 23 | set(value) = definedExternally 24 | } 25 | 26 | internal external interface MoveOptions { 27 | var force: Boolean? 28 | get() = definedExternally 29 | set(value) = definedExternally 30 | } 31 | 32 | internal external fun cp(source: String, dest: String, options: CopyOptions = definedExternally): Promise 33 | 34 | internal external fun mv(source: String, dest: String, options: MoveOptions = definedExternally): Promise 35 | 36 | internal external fun rmRF(inputPath: String): Promise 37 | 38 | internal external fun mkdirP(fsPath: String): Promise 39 | 40 | internal external fun which(tool: String, check: Boolean = definedExternally): Promise 41 | 42 | internal external fun findInPath(tool: String): Promise> -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/artifact/Responses.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | @file:JsModule("@actions/artifact") 8 | @file:JsNonModule 9 | 10 | package com.rnett.action.artifact 11 | 12 | /** 13 | * The response to an artifact upload. 14 | */ 15 | public external interface UploadResponse { 16 | /** 17 | * The name of the artifact that was uploaded 18 | */ 19 | public val artifactName: String 20 | 21 | /** 22 | * A list of all items that are meant to be uploaded as part of the artifact 23 | */ 24 | public val artifactItems: Array 25 | 26 | /** 27 | * Total size of the artifact in bytes that was uploaded 28 | */ 29 | public val size: Number 30 | 31 | /** 32 | * A list of items that were not uploaded as part of the artifact (includes queued items that were not uploaded if 33 | * continueOnError is set to false). This is a subset of artifactItems. 34 | */ 35 | public val failedItems: Array 36 | } 37 | 38 | /** 39 | * The response of an artifact download. 40 | */ 41 | public external interface DownloadResponse { 42 | /** 43 | * The name of the artifact that was downloaded 44 | */ 45 | public val artifactName: String 46 | 47 | /** 48 | * The full Path to where the artifact was downloaded 49 | */ 50 | public val downloadPath: String 51 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/github/types.module_@octokit_core.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | @file:JsModule("@octokit/core") 3 | @file:JsNonModule 4 | 5 | package internal.github 6 | 7 | import kotlin.js.* 8 | 9 | internal external interface `T$11` { 10 | var debug: (message: String) -> Any 11 | var info: (message: String) -> Any 12 | var warn: (message: String) -> Any 13 | var error: (message: String) -> Any 14 | } 15 | 16 | internal external interface OctokitOptions { 17 | var authStrategy: Any? 18 | get() = definedExternally 19 | set(value) = definedExternally 20 | var auth: Any? 21 | get() = definedExternally 22 | set(value) = definedExternally 23 | var userAgent: String? 24 | get() = definedExternally 25 | set(value) = definedExternally 26 | var previews: Array? 27 | get() = definedExternally 28 | set(value) = definedExternally 29 | var baseUrl: String? 30 | get() = definedExternally 31 | set(value) = definedExternally 32 | var log: `T$11`? 33 | get() = definedExternally 34 | set(value) = definedExternally 35 | var request: RequestRequestOptions? 36 | get() = definedExternally 37 | set(value) = definedExternally 38 | var timeZone: String? 39 | get() = definedExternally 40 | set(value) = definedExternally 41 | @nativeGetter 42 | operator fun get(option: String): Any? 43 | @nativeSetter 44 | operator fun set(option: String, value: Any) 45 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/core/outputs.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.core 2 | 3 | import com.rnett.action.camelToSnakeCase 4 | import kotlin.properties.ReadWriteProperty 5 | import kotlin.reflect.KProperty 6 | 7 | internal class OutputDelegate(val name: String?) : ReadWriteProperty { 8 | private var value: String? = null 9 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { 10 | this.value = value 11 | outputs.set(name ?: property.name.camelToSnakeCase(), value) 12 | } 13 | 14 | override fun getValue(thisRef: Any?, property: KProperty<*>): String { 15 | return value ?: "" 16 | } 17 | } 18 | 19 | /** 20 | * A setter to set outputs. 21 | * 22 | * Can be delegated from. Property names will be converted to snake-case unless name is specified. 23 | * Delegates return `""` if the delegate hasn't been set, or the value set by that delegate if it has been used. 24 | */ 25 | public object outputs : ReadWriteProperty by OutputDelegate(null) { 26 | 27 | /** 28 | * Get a delegate for [name]. 29 | * 30 | * Delegates return `""` if the delegate hasn't been set, or the value set by that delegate if it has been used. 31 | */ 32 | public operator fun invoke(name: String): ReadWriteProperty = OutputDelegate(name) 33 | 34 | /** 35 | * Set an output [name] to [value]. 36 | */ 37 | public operator fun set(name: String, value: String) { 38 | core.setOutput(name, value) 39 | } 40 | 41 | /** 42 | * Set all outputs from [outputs]. 43 | */ 44 | public fun setAll(outputs: Map) { 45 | outputs.forEach { (k, v) -> set(k, v) } 46 | } 47 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/github/index.module_@octokit_core.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | @file:JsModule("@octokit/core") 3 | @file:JsNonModule 4 | 5 | package internal.github 6 | 7 | import internal.HookCollection 8 | import kotlin.js.* 9 | 10 | internal external interface `T$9` { 11 | var plugins: Array 12 | } 13 | 14 | internal external interface `T$10` { 15 | var debug: (message: String, additionalInfo: Any?) -> Any 16 | var info: (message: String, additionalInfo: Any?) -> Any 17 | var warn: (message: String, additionalInfo: Any?) -> Any 18 | var error: (message: String, additionalInfo: Any?) -> Any 19 | @nativeGetter 20 | operator fun get(key: String): Any? 21 | @nativeSetter 22 | operator fun set(key: String, value: Any) 23 | } 24 | 25 | internal external open class Octokit(options: OctokitOptions = definedExternally) { 26 | open var request: Any 27 | open var graphql: Any 28 | open var log: `T$10` 29 | open var hook: HookCollection 30 | open var auth: (args: Any) -> Promise 31 | @nativeGetter 32 | open operator fun get(key: String): Any? 33 | @nativeSetter 34 | open operator fun set(key: String, value: Any) 35 | 36 | companion object { 37 | var VERSION: String 38 | fun > defaults(self: S, defaults: OctokitOptions): Any /* Any & S */ 39 | fun > defaults(self: S, defaults: Function<*>): Any /* Any & S */ 40 | var plugins: Array 41 | fun , T : Array> plugin(self: S, vararg newPlugins: T): `T$9` /* `T$9` & S & Constructor>> */ 42 | } 43 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/httpclient/HttpResponse.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.httpclient 2 | 3 | import internal.httpclient.HttpClientResponse 4 | import kotlinx.coroutines.GlobalScope 5 | import kotlinx.coroutines.await 6 | import kotlinx.coroutines.promise 7 | import node.http.IncomingMessage 8 | import kotlin.js.Promise 9 | 10 | 11 | /** 12 | * The response from a HTTP request. 13 | */ 14 | public interface HttpResponse { 15 | public suspend fun readBody(): String 16 | 17 | public val message: IncomingMessage 18 | public val headers: Headers 19 | public val statusCode: Int 20 | public val statusMessage: String 21 | public fun isSuccess(): Boolean 22 | } 23 | 24 | internal fun HttpResponse.toInternal(): HttpClientResponse = 25 | if (this is HttpResponseImpl) 26 | this.internal 27 | else 28 | object : HttpClientResponse(this@toInternal.message) { 29 | override var message: IncomingMessage = this@toInternal.message 30 | 31 | override fun readBody(): Promise = GlobalScope.promise { 32 | this@toInternal.readBody() 33 | } 34 | } 35 | 36 | 37 | /** 38 | * The response from a HTTP request. 39 | */ 40 | internal class HttpResponseImpl internal constructor(internal val internal: HttpClientResponse) : HttpResponse { 41 | override suspend fun readBody(): String = internal.readBody().await() 42 | 43 | override val message: IncomingMessage get() = internal.message 44 | 45 | override val headers: Headers = message.rawHeaders.asSequence().chunked(2) { 46 | it[0].lowercase() to it[1] 47 | }.toMap().let { MapHeaders(it) } 48 | 49 | override val statusCode: Int = message.statusCode!!.toInt() 50 | override val statusMessage: String get() = message.statusMessage!! 51 | override fun isSuccess(): Boolean = statusCode in 200..299 52 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/test/kotlin/com/rnett/action/TestStreamUtils.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import kotlinx.coroutines.ExperimentalCoroutinesApi 4 | import kotlinx.coroutines.flow.flowOf 5 | import kotlinx.coroutines.flow.toList 6 | import kotlinx.coroutines.test.runTest 7 | import node.events.Event 8 | import node.stream.Transform 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | 12 | @OptIn(ExperimentalCoroutinesApi::class) 13 | class TestStreamUtils { 14 | 15 | @Test 16 | fun testStreamToFlow() = runTest { 17 | val stream = Transform(JsObject { 18 | this.writableObjectMode = true 19 | this.readableObjectMode = true 20 | 21 | transform = { chunk, encoding, callback -> 22 | callback(null, chunk) 23 | } 24 | }) 25 | 26 | stream.write(listOf(1, 2, 3)) 27 | val flow = stream.toFlow>() 28 | 29 | stream.write(listOf(3, 4, 5)) 30 | stream.end() 31 | 32 | val lists = flow.toList() 33 | 34 | assertEquals( 35 | listOf( 36 | listOf(1, 2, 3), 37 | listOf(3, 4, 5) 38 | ), lists 39 | ) 40 | } 41 | 42 | @Test 43 | fun testObjectStream() = runTest { 44 | val flow = flowOf(listOf(1, 2, 3), listOf(3, 4, 5)) 45 | val stream = flow.toObjectStream(this) 46 | var i = 0 47 | stream.on(Event.DATA) { it -> 48 | if (i == 0) 49 | assertEquals(listOf(1, 2, 3), it) 50 | else 51 | assertEquals(listOf(3, 4, 5), it) 52 | i++ 53 | } 54 | stream.resume() 55 | } 56 | 57 | @Test 58 | fun canCreateBuffersFromEncodedData() { 59 | assertEquals( 60 | "test", 61 | "test".encodeBase64().decodeBase64() 62 | ) 63 | } 64 | } -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/docs/src/main/kotlin/com/rnett/action/DocsRootPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | import org.gradle.api.tasks.Copy 6 | import org.gradle.kotlin.dsl.create 7 | import org.gradle.kotlin.dsl.withType 8 | 9 | class DocsRootPlugin : Plugin { 10 | override fun apply(project: Project): Unit = with(project) { 11 | project.plugins.apply(MetadataPlugin::class.java) 12 | project.plugins.apply("org.jetbrains.dokka") 13 | 14 | val extension = project.extensions.create("docs", RootDocsExtension::class.java) 15 | 16 | val oldVersionsDir = providers.gradleProperty("oldVersionsDir") 17 | 18 | tasks.withType().configureEach { 19 | this.fileLayout.set(org.jetbrains.dokka.gradle.DokkaMultiModuleFileLayout.CompactInParent) 20 | this.includes.from("DOCS.md") 21 | this.moduleName.set(extension.title) 22 | this.moduleVersion.set(version.toString()) 23 | if (oldVersionsDir.isPresent && "snapshot" !in project.version.toString().toLowerCase()) { 24 | val resolved = rootDir.resolve(oldVersionsDir.get()) 25 | println("Using older versions from $resolved") 26 | pluginConfiguration { 27 | version = project.version.toString() 28 | olderVersionsDir = resolved 29 | } 30 | } 31 | } 32 | 33 | tasks.create("generateReadme") { 34 | from(rootDir.resolve("../README.md")) 35 | into(buildDir.resolve("readme")) 36 | filter { 37 | val header = extension.readmeHeader.get() 38 | it.replace( 39 | "# $header", 40 | "# [$header](https://github.com/rnett/kotlin-js-action)" 41 | ) 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/OperatingSystem.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import node.path.path 4 | import node.process.Platform 5 | 6 | /** 7 | * Operating system Enum. Limited to GitHub action runners (Windows, Mac, and Linux). 8 | */ 9 | public enum class OperatingSystem { 10 | Windows, Mac, Linux; 11 | 12 | public companion object { 13 | 14 | /** 15 | * The current operating system. 16 | */ 17 | public val current: OperatingSystem by lazy { 18 | when (node.os.platform()) { 19 | Platform.win32 -> Windows 20 | Platform.darwin -> Mac 21 | else -> Linux 22 | } 23 | } 24 | 25 | /** 26 | * Whether the current OS is Windows 27 | */ 28 | public inline val isWindows: Boolean get() = current == Windows 29 | 30 | /** 31 | * Whether the current OS is Max 32 | */ 33 | public inline val isMac: Boolean get() = current == Mac 34 | 35 | /** 36 | * Whether the current OS is Linux 37 | */ 38 | public inline val isLinux: Boolean get() = current == Linux 39 | 40 | /** 41 | * Whether the current OS is POSIX compliant, i.e. Linux or Mac 42 | */ 43 | public inline val isPosix: Boolean get() = !isWindows 44 | 45 | /** 46 | * The line separator of the current operating system 47 | */ 48 | public inline val lineSeparator: String 49 | get() = when (current) { 50 | Windows -> "\r\n" 51 | Mac -> "\r" 52 | Linux -> "\n" 53 | } 54 | 55 | /** 56 | * Get the current OS's path seperator 57 | */ 58 | public inline val pathSeperator: String get() = path.sep 59 | 60 | /** 61 | * Get [os.arch]. 62 | */ 63 | public inline val arch: String get() = node.os.arch() 64 | 65 | /** 66 | * Get [os.platform] 67 | */ 68 | public inline val platform: String get() = node.os.platform().name 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/test/kotlin/com/rnett/action/TestWithDir.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import kotlinx.coroutines.ExperimentalCoroutinesApi 4 | import kotlinx.coroutines.delay 5 | import kotlinx.coroutines.launch 6 | import kotlinx.coroutines.test.TestResult 7 | import kotlinx.coroutines.test.TestScope 8 | import kotlinx.coroutines.test.advanceUntilIdle 9 | import kotlin.random.Random 10 | import kotlin.test.AfterTest 11 | import kotlin.test.BeforeTest 12 | import kotlin.time.Duration.Companion.milliseconds 13 | 14 | val globalTestDir by lazy { Path(TestEnv.tempDir.trimEnd('/') + "/testdir") } 15 | 16 | @OptIn(ExperimentalCoroutinesApi::class) 17 | abstract class TestWithDir { 18 | 19 | private var _testDir: Path? = null 20 | 21 | val testDir: Path 22 | get() = _testDir ?: error("Test dir not set yet") 23 | 24 | fun runTest(block: suspend TestScope.() -> Unit): TestResult { 25 | return kotlinx.coroutines.test.runTest { 26 | doInit() 27 | block() 28 | } 29 | } 30 | 31 | @BeforeTest 32 | fun before() = kotlinx.coroutines.test.runTest { 33 | doInit() 34 | } 35 | 36 | private suspend fun doInit() { 37 | if (_testDir != null) { 38 | return 39 | } 40 | 41 | val name = Random.nextLong().toString(20) + Random.nextLong().toString(20) 42 | val dir = globalTestDir / "test-$name" 43 | 44 | if (dir.exists) { 45 | node.fs.rmdirSync(dir.path, JsObject { 46 | this.recursive = true 47 | }) 48 | } 49 | 50 | dir.mkdir() 51 | dir.initDir() 52 | if (_testDir == null) { 53 | _testDir = dir 54 | Path.cd(dir) 55 | } 56 | } 57 | 58 | open suspend fun Path.initDir() { 59 | 60 | } 61 | 62 | @AfterTest 63 | internal fun after() = kotlinx.coroutines.test.runTest { 64 | backgroundScope.launch { 65 | delay(500.milliseconds) 66 | node.fs.rmdirSync(testDir.resolve().path, JsObject { 67 | this.recursive = true 68 | }) 69 | } 70 | advanceUntilIdle() 71 | } 72 | } -------------------------------------------------------------------------------- /kotlin-js-action/serialization/src/test/kotlin/com/rnett/action/serialization/TestJsonHttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.serialization 2 | 3 | import com.rnett.action.httpclient.use 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.flow.flowOf 6 | import kotlinx.coroutines.test.runTest 7 | import kotlinx.serialization.Serializable 8 | import kotlinx.serialization.json.Json 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | 12 | @Serializable 13 | data class TestResponse(val slideshow: Slideshow) 14 | 15 | @Serializable 16 | data class Slideshow(val title: String, val author: String, val date: String, val slides: List) 17 | 18 | @Serializable 19 | data class Slide(val title: String, val type: String, val items: List = emptyList()) 20 | 21 | @Serializable 22 | data class TestPostResponse(val json: T) 23 | 24 | @OptIn(ExperimentalCoroutinesApi::class) 25 | class HttpClientTest { 26 | @Test 27 | fun testGet() = runTest { 28 | JsonHttpClient().use { client -> 29 | val response = client.get("https://httpbin.org/json") 30 | assertEquals(200, response.statusCode) 31 | val data = response.readJsonBody() 32 | assertEquals("Yours Truly", data.slideshow.author) 33 | } 34 | } 35 | 36 | @Test 37 | fun testPost() = runTest { 38 | JsonHttpClient(Json { 39 | this.ignoreUnknownKeys = true 40 | }).use { client -> 41 | val slide = Slide("Nothing", "all") 42 | val response = client.postJson("https://httpbin.org/post", slide) 43 | assertEquals(200, response.statusCode) 44 | val data = response.readJsonBody>().json 45 | assertEquals(slide, data) 46 | } 47 | } 48 | 49 | @Test 50 | fun testPostStreaming() = runTest { 51 | JsonHttpClient(Json { 52 | this.ignoreUnknownKeys = true 53 | }).use { client -> 54 | val slide = Slide("Nothing", "all") 55 | val slides = flowOf(slide) 56 | val response = client.postJson("https://httpbin.org/post", slides) 57 | assertEquals(200, response.statusCode) 58 | val data = response.readJsonBody>().json 59 | assertEquals(slide, data) 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /.gradle/ 3 | /build/ 4 | /testdir/ 5 | /docs/ 6 | 7 | log 8 | **/build 9 | 10 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 11 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 12 | 13 | # User-specific stuff 14 | .idea/**/workspace.xml 15 | .idea/**/tasks.xml 16 | .idea/**/usage.statistics.xml 17 | .idea/**/dictionaries 18 | .idea/**/shelf 19 | 20 | # Generated files 21 | .idea/**/contentModel.xml 22 | 23 | # Sensitive or high-churn files 24 | .idea/**/dataSources/ 25 | .idea/**/dataSources.ids 26 | .idea/**/dataSources.local.xml 27 | .idea/**/sqlDataSources.xml 28 | .idea/**/dynamic.xml 29 | .idea/**/uiDesigner.xml 30 | .idea/**/dbnavigator.xml 31 | 32 | # Gradle 33 | .idea/**/gradle.xml 34 | .idea/**/libraries 35 | 36 | # Gradle and Maven with auto-import 37 | # When using Gradle or Maven with auto-import, you should exclude module files, 38 | # since they will be recreated, and may cause churn. Uncomment if using 39 | # auto-import. 40 | # .idea/artifacts 41 | # .idea/compiler.xml 42 | # .idea/modules.xml 43 | # .idea/*.iml 44 | # .idea/modules 45 | # *.iml 46 | # *.ipr 47 | 48 | # CMake 49 | cmake-build-*/ 50 | 51 | # Mongo Explorer plugin 52 | .idea/**/mongoSettings.xml 53 | 54 | # File-based project format 55 | *.iws 56 | 57 | # IntelliJ 58 | out/ 59 | 60 | # mpeltonen/sbt-idea plugin 61 | .idea_modules/ 62 | 63 | # JIRA plugin 64 | atlassian-ide-plugin.xml 65 | 66 | # Cursive Clojure plugin 67 | .idea/replstate.xml 68 | 69 | # Crashlytics plugin (for Android Studio and IntelliJ) 70 | com_crashlytics_export_strings.xml 71 | crashlytics.properties 72 | crashlytics-build.properties 73 | fabric.properties 74 | 75 | # Editor-based Rest Client 76 | .idea/httpRequests 77 | 78 | # Android studio 3.1+ serialized cache file 79 | .idea/caches/build_file_checksums.ser 80 | 81 | .gradle 82 | /build/ 83 | 84 | # Ignore Gradle GUI config 85 | gradle-app.setting 86 | 87 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 88 | !gradle-wrapper.jar 89 | 90 | # Cache of project 91 | .gradletasknamecache 92 | 93 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 94 | # gradle/wrapper/gradle-wrapper.properties 95 | /.idea/google-java-format.xml 96 | /.idea/GitLink.xml 97 | /.idea/checkstyle-idea.xml 98 | /.idea/git_toolbox_prj.xml 99 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/core/AnnotationProperties.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.core 2 | 3 | import com.rnett.action.JsObject 4 | import com.rnett.action.Path 5 | 6 | /** 7 | * Properties for GitHub actions state annotations. Can be used with some logging methods to show UI indications. 8 | * 9 | * See [the docs](https://github.com/actions/toolkit/tree/main/packages/core#annotations). 10 | */ 11 | public data class AnnotationProperties internal constructor( 12 | val title: String, 13 | val file: Path?, 14 | val startLine: Int?, 15 | val endLine: Int?, 16 | val startColumn: Int?, 17 | val endColumn: Int? 18 | ) { 19 | /** 20 | * Create an annotation with no location information. 21 | */ 22 | public constructor(title: String) : this(title, null, null, null, null, null) 23 | 24 | internal fun toJsObject() = JsObject { 25 | this.title = this@AnnotationProperties.title 26 | this.startLine = this@AnnotationProperties.startLine 27 | this.endLine = this@AnnotationProperties.endLine 28 | this.startColumn = this@AnnotationProperties.startColumn 29 | this.endColumn = this@AnnotationProperties.endColumn 30 | this.file = this@AnnotationProperties.file?.path 31 | } 32 | 33 | public companion object { 34 | 35 | /** 36 | * Create an annotation with a single line location, optionally with start and end columns. 37 | */ 38 | public fun singleLine( 39 | title: String, 40 | file: Path, 41 | line: Int, 42 | startColumn: Int? = null, 43 | endColumn: Int? = startColumn 44 | ): AnnotationProperties = AnnotationProperties(title, file, line, line, startColumn, endColumn) 45 | 46 | /** 47 | * Create an annotation with a multi-line location. 48 | */ 49 | public fun multiLine( 50 | title: String, 51 | file: Path, 52 | startLine: Int, 53 | endLine: Int, 54 | ): AnnotationProperties = AnnotationProperties(title, file, startLine, endLine, null, null) 55 | 56 | /** 57 | * Create an annotation with no location information. 58 | */ 59 | public fun noLine(title: String, file: Path? = null): AnnotationProperties = AnnotationProperties(title, file, null, null, null, null) 60 | } 61 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/httpclient/interfaces.module_@actions_http-client.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | @file:JsModule("@actions/http-client") 8 | @file:JsNonModule 9 | 10 | package internal.httpclient 11 | 12 | import node.http.IncomingHttpHeaders 13 | import node.http.OutgoingHttpHeaders 14 | import org.w3c.dom.url.URL 15 | import kotlin.js.Promise 16 | 17 | internal external interface RequestOptions { 18 | var headers: OutgoingHttpHeaders? 19 | get() = definedExternally 20 | set(value) = definedExternally 21 | var socketTimeout: Number? 22 | get() = definedExternally 23 | set(value) = definedExternally 24 | var ignoreSslError: Boolean? 25 | get() = definedExternally 26 | set(value) = definedExternally 27 | var allowRedirects: Boolean? 28 | get() = definedExternally 29 | set(value) = definedExternally 30 | var allowRedirectDowngrade: Boolean? 31 | get() = definedExternally 32 | set(value) = definedExternally 33 | var maxRedirects: Number? 34 | get() = definedExternally 35 | set(value) = definedExternally 36 | var maxSockets: Number? 37 | get() = definedExternally 38 | set(value) = definedExternally 39 | var keepAlive: Boolean? 40 | get() = definedExternally 41 | set(value) = definedExternally 42 | var deserializeDates: Boolean? 43 | get() = definedExternally 44 | set(value) = definedExternally 45 | var allowRetries: Boolean? 46 | get() = definedExternally 47 | set(value) = definedExternally 48 | var maxRetries: Number? 49 | get() = definedExternally 50 | set(value) = definedExternally 51 | } 52 | 53 | internal external interface RequestHandler { 54 | fun prepareRequest(options: node.http.RequestOptions) 55 | fun canHandleAuthentication(response: HttpClientResponse): Boolean 56 | fun handleAuthentication(httpClient: HttpClient, requestInfo: RequestInfo, data: Any?): Promise 57 | } 58 | 59 | internal external interface RequestInfo { 60 | var options: node.http.RequestOptions 61 | var parsedUrl: URL 62 | var httpModule: Any 63 | } 64 | 65 | internal external interface TypedResponse { 66 | var statusCode: Number 67 | var result: T? 68 | var headers: IncomingHttpHeaders 69 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/test/resources/archives/test.tar: -------------------------------------------------------------------------------- 1 | test/0040777000000000000000000000000014072432207006774 5ustar00test/file.txt0100777000000000000000000000001114072432213010441 0ustar00Test file -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action-plugin/src/main/kotlin/com/rnett/action/KotlinAction.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.Task 5 | import org.gradle.api.file.Directory 6 | import org.gradle.api.tasks.TaskProvider 7 | import org.gradle.kotlin.dsl.the 8 | import org.gradle.kotlin.dsl.withType 9 | import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl 10 | import java.io.File 11 | 12 | /** 13 | * Add a task to create the custom webpack config necessary for packing GitHub actions. Done automatically in [githubAction]. 14 | */ 15 | fun Project.addWebpackGenTask(): TaskProvider = tasks.register(Constants.createWebpackTaskName) { 16 | group = Constants.taskGroup 17 | val directory = File("$projectDir/webpack.config.d/") 18 | val outputFile = File("$directory/github.action.config.js") 19 | doLast { 20 | if (!directory.exists()) 21 | directory.mkdir() 22 | 23 | outputFile.writeText("config.target = 'node';") 24 | } 25 | outputs.file(outputFile) 26 | .withPropertyName("outputFile") 27 | } 28 | 29 | /** 30 | * Adds a JS target for GitHub actions (browser commonJs w/ node libraries) that run using `node12` and configures necessary tasks for packing. 31 | * 32 | * Running the production webpack task will generate the compiled GitHub task in [outputDir]/[outFileName], which by default is `dist/index.js`. 33 | */ 34 | fun KotlinJsTargetDsl.githubAction( 35 | outputDir: Directory = project.layout.projectDirectory.dir("dist"), 36 | outFileName: String = "index.js", 37 | nodeVersion: String = "16.18.0" 38 | ) { 39 | 40 | useCommonJs() 41 | 42 | val webpackGenTask = project.addWebpackGenTask() 43 | binaries.executable() 44 | 45 | browser { 46 | distribution { 47 | this.directory = outputDir.asFile 48 | this.name = outFileName 49 | } 50 | webpackTask { 51 | if (mode == org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig.Mode.PRODUCTION) { 52 | output.globalObject = "this" // NodeJS mode 53 | sourceMaps = false 54 | this.outputFileName = outFileName 55 | 56 | dependsOn(webpackGenTask) 57 | } 58 | } 59 | } 60 | 61 | project.rootProject.plugins.withType { 62 | project.rootProject.the().nodeVersion = nodeVersion 63 | } 64 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/core/inputs.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.core 2 | 3 | import com.rnett.action.delegates.Delegatable 4 | import com.rnett.action.delegates.ifNull 5 | import kotlin.properties.ReadOnlyProperty 6 | import kotlin.reflect.KProperty 7 | 8 | /** 9 | * Accessors for input variables. 10 | * 11 | * Can be delegated from. Property names will be converted to snake-case unless name is specified. 12 | * Delegating from [inputs] treats the input as required. 13 | */ 14 | public object inputs : Delegatable(true), ReadOnlyProperty { 15 | 16 | /** 17 | * Get the input passed for [name], or throws an error if it was not passed. 18 | */ 19 | public override fun getRequired(name: String): String = core.getRequiredInput(name) 20 | 21 | /** 22 | * Get the input passed for [name], or throws an error if it was not passed. 23 | */ 24 | public override fun getOptional(name: String): String? = core.getOptionalInput(name) 25 | 26 | /** 27 | * Get the input passed for [name]. Treats it as optional, see [getOptional]. 28 | */ 29 | public operator fun get(name: String): String? = getOptional(name) 30 | 31 | /** 32 | * A delegate based on the property name converted to snake case, for a required input. 33 | */ 34 | override fun getValue(thisRef: Any?, property: KProperty<*>): String { 35 | return getRequired(property.name.delegateName()) 36 | } 37 | 38 | /** 39 | * Get a delegate for [name]. 40 | */ 41 | public operator fun invoke(name: String): ReadOnlyProperty = delegate(name) 42 | 43 | /** 44 | * Get an optional delegate. Property names will be converted to snake-case unless name is specified. 45 | */ 46 | public val optional: ReadOnlyProperty by lazy { optionalDelegate(null) } 47 | 48 | /** 49 | * Get an optional delegate for [name]. 50 | */ 51 | public fun optional(name: String): ReadOnlyProperty = optionalDelegate(name) 52 | 53 | /** 54 | * Get an optional delegate with a default value. Property names will be converted to snake-case unless name is specified. 55 | */ 56 | public fun optionalWithDefault(default: () -> String): ReadOnlyProperty = optional.ifNull(default) 57 | 58 | /** 59 | * Get an optional delegate with a default value for [name]. 60 | */ 61 | public fun optionalWithDefault(name: String, default: () -> String): ReadOnlyProperty = 62 | optional(name).ifNull(default) 63 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/test/kotlin/com/rnett/action/TestExec.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import com.rnett.action.exec.exec 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.runTest 6 | import node.buffer.Buffer 7 | import node.buffer.BufferEncoding 8 | import kotlin.test.Test 9 | import kotlin.test.assertEquals 10 | import kotlin.test.assertTrue 11 | 12 | @OptIn(ExperimentalCoroutinesApi::class) 13 | class TestExec : TestWithDir() { 14 | override suspend fun Path.initDir() { 15 | descendant("testFile3").touch().write("Testing file") 16 | } 17 | 18 | val diff = if (OperatingSystem.isWindows) "\r\n" else "" 19 | 20 | @Test 21 | fun testExec() = runTest { 22 | exec.execCommand("javac", "--version") 23 | } 24 | 25 | @Test 26 | fun testExecAndCapture() = runTest { 27 | assertTrue(exec.execCommandAndCapture("javac", "--version").stdout.startsWith("javac")) 28 | } 29 | 30 | @Test 31 | fun testExecShell() = runTest { 32 | exec.execShell("cp testFile3 testOut1", cwd = testDir) 33 | val file = (testDir / "testOut1") 34 | assertEquals("Testing file", file.readText()) 35 | } 36 | 37 | @Test 38 | fun testExecAndCaptureShell() = runTest { 39 | assertEquals("Testing file$diff", exec.execShellAndCapture("cat testFile3 | cat", cwd = testDir).stdout) 40 | } 41 | 42 | @Test 43 | fun testInputRedirect() = runTest { 44 | if (!OperatingSystem.isWindows) { 45 | val output = exec.execShellAndCapture("cat", input = Buffer.from("Test")) 46 | assertEquals("Test", output.throwIfFailure().stdout) 47 | } 48 | } 49 | 50 | @Test 51 | fun testOutputRedirect() = runTest { 52 | val outputFile = (testDir / "testOut3") 53 | val stream = outputFile.writeStream() 54 | exec.execShell("echo \"Test\"", outStream = stream) 55 | stream.close() 56 | assertEquals("Test", outputFile.readText().lines()[1]) 57 | } 58 | 59 | @Test 60 | fun testOutputInShellRedirect() = runTest { 61 | val shellDiff = if (OperatingSystem.isWindows) "\r\n" else "\n" 62 | 63 | val outputFile = (testDir / "testOut2") 64 | exec.execShell("echo \"Test\" > \"$outputFile\"") 65 | assertEquals("Test$shellDiff", outputFile.readText(if (OperatingSystem.isWindows) BufferEncoding.ucs2 else BufferEncoding.utf8).let { 66 | if (OperatingSystem.isWindows) 67 | it.substring(1) 68 | else 69 | it 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /kotlin-js-action/build-logic/docs/src/main/kotlin/com/rnett/action/DocsLeafPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | import org.gradle.kotlin.dsl.create 6 | import org.gradle.kotlin.dsl.withType 7 | import java.net.URL 8 | 9 | class DocsLeafPlugin : Plugin { 10 | override fun apply(project: Project): Unit = with(project) { 11 | project.plugins.apply(MetadataPlugin::class.java) 12 | project.plugins.apply("org.jetbrains.dokka") 13 | val extension = project.extensions.create("docs", LeafDocsExtension::class) 14 | 15 | tasks.withType() { 16 | moduleName.set(extension.title) 17 | moduleVersion.set(version.toString()) 18 | 19 | dokkaSourceSets.configureEach { 20 | includes.from(listOf(file("module.md"), file("packages.md"), file("README.md")).filter { it.exists() }) 21 | includeNonPublic.set(false) 22 | suppressObviousFunctions.set(true) 23 | suppressInheritedMembers.set(false) 24 | skipDeprecated.set(false) 25 | skipEmptyPackages.set(true) 26 | jdkVersion.set(8) 27 | 28 | val sourceSet = this.sourceSetID.sourceSetName 29 | 30 | sourceLink { 31 | localDirectory.set(file("src/$sourceSet/kotlin")) 32 | 33 | remoteUrl.set(project.providers.gradleProperty("sourceLinkBranch").map { sourceLinkBranch -> 34 | 35 | val githubRoot = buildString { 36 | append("https://github.com/rnett/kotlin-js-action/blob/") 37 | append(sourceLinkBranch) 38 | append("/kotlin-js-action/") 39 | 40 | val dir = project.projectDir.relativeTo(rootProject.projectDir).path.trim('/') 41 | 42 | append("/$dir") 43 | } 44 | 45 | java.net.URL("$githubRoot/src/$sourceSet/kotlin") 46 | }) 47 | remoteLineSuffix.set("#L") 48 | } 49 | 50 | platform.set(extension.platform) 51 | 52 | externalDocumentationLink { 53 | url.set(URL("https://kotlin.github.io/kotlinx.coroutines/")) 54 | } 55 | 56 | externalDocumentationLink { 57 | url.set(URL("https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-json/kotlinx-serialization-json/")) 58 | } 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/test/kotlin/com/rnett/action/TestToolCache.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import com.rnett.action.core.isActionRunner 4 | import com.rnett.action.toolcache.toolcache 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertNotNull 9 | 10 | @OptIn(ExperimentalCoroutinesApi::class) 11 | class TestToolCache : TestWithDir() { 12 | 13 | val licenseUrl = "https://raw.githubusercontent.com/rnett/kotlin-js-action/main/LICENSE" 14 | suspend fun localLicense() = (Path(TestEnv.projectDirPath) / "LICENSE").readText().replace("\r\n", "\n") 15 | 16 | @Test 17 | fun testDownload() = runTest { 18 | val resultFile = testDir / "testLicense" 19 | val result = toolcache.downloadTool(licenseUrl, resultFile) 20 | assertEquals(result, resultFile) 21 | assertEquals(localLicense(), resultFile.readText()) 22 | 23 | if (isActionRunner) { 24 | val tempResult = toolcache.downloadTool(licenseUrl) 25 | assertEquals(localLicense(), tempResult.readText().replace("\r\n", "\n")) 26 | } 27 | } 28 | 29 | @Test 30 | fun testExtract() = runTest { 31 | val archivesDir = Path(TestEnv.projectDirPath) / "kotlin-js-action/kotlin-js-action/src/test/resources/archives" 32 | val extractedDir = testDir / "testExtract" 33 | archivesDir.children().forEach { 34 | 35 | if (it.name.endsWith(".7z") && !OperatingSystem.isWindows) 36 | return@forEach 37 | 38 | val extracted = toolcache.extract(it, extractedDir) 39 | assertEquals("Test file", (extracted / "test/file.txt").readText()) 40 | extractedDir.delete(true) 41 | } 42 | } 43 | 44 | @Test 45 | fun testCacheDir() = runTest { 46 | if (!isActionRunner) return@runTest 47 | 48 | val dir = testDir / "cacheDir" 49 | toolcache.downloadTool(licenseUrl, dir / "test") 50 | 51 | toolcache.cacheDir(dir, "testTool", "1.0.1") 52 | toolcache.cacheDir(dir, "testTool", "1.0.2") 53 | 54 | val found = toolcache.find("testTool", "1.x.x") 55 | assertNotNull(found) 56 | 57 | assertEquals(listOf("1.0.1", "1.0.2"), toolcache.findAllVersions("testTool").sorted()) 58 | } 59 | 60 | @Test 61 | fun testCacheFile() = runTest { 62 | if (!isActionRunner) return@runTest 63 | 64 | toolcache.downloadTool(licenseUrl, testDir / "test") 65 | 66 | toolcache.cacheFile(testDir / "test", "testFileTool", "1.0.1") 67 | 68 | val found = toolcache.find("testFileTool", "1.x.x") 69 | assertNotNull(found) 70 | } 71 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/test/kotlin/com/rnett/action/TestGlob.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import com.rnett.action.glob.glob 4 | import com.rnett.action.glob.globFlow 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.toList 7 | import kotlin.test.Test 8 | import kotlin.test.assertEquals 9 | 10 | @OptIn(ExperimentalCoroutinesApi::class) 11 | class TestGlob : TestWithDir() { 12 | override suspend fun Path.initDir() { 13 | descendant("testDir1/innerDir1/file1").touch() 14 | descendant("testDir1/innerDir1/file2").touch() 15 | descendant("testDir1/innerDir1/file3").touch() 16 | descendant("testDir1/innerDir2/file1").touch() 17 | descendant("testDir1/innerDir2/file2").touch() 18 | descendant("testDir2/innerDir1/file1").touch() 19 | descendant("testDir2/innerDir1/file2").touch() 20 | descendant("testDir2/innerDir1/file3").touch() 21 | descendant("testDir2/innerDir2/file1").touch() 22 | } 23 | 24 | private suspend fun tryGlob( 25 | vararg patterns: String, 26 | followSymbolicLinks: Boolean = true, 27 | implicitDescendants: Boolean = true, 28 | omitBrokenSymbolicLinks: Boolean = true, 29 | matchDirectories: Boolean = true 30 | ): List { 31 | val glob = glob( 32 | *patterns, 33 | followSymbolicLinks = followSymbolicLinks, 34 | implicitDescendants = implicitDescendants, 35 | omitBrokenSymbolicLinks = omitBrokenSymbolicLinks, 36 | matchDirectories = matchDirectories 37 | ) 38 | val globFlow = globFlow( 39 | *patterns, 40 | followSymbolicLinks = followSymbolicLinks, 41 | implicitDescendants = implicitDescendants, 42 | omitBrokenSymbolicLinks = omitBrokenSymbolicLinks, 43 | matchDirectories = matchDirectories 44 | ).toList() 45 | assertEquals(glob.toSet(), globFlow.toSet(), "Glob and Glob flow did not match") 46 | return glob 47 | } 48 | 49 | @Test 50 | fun getFiles() = runTest { 51 | val files = tryGlob("./**/file*") 52 | assertEquals(9, files.size) 53 | } 54 | 55 | @Test 56 | fun byDir() = runTest { 57 | val files = tryGlob("./**/innerDir*") 58 | assertEquals(13, files.size) 59 | } 60 | 61 | @Test 62 | fun byDirNoDirs() = runTest { 63 | val files = tryGlob("./**/innerDir*", matchDirectories = false) 64 | assertEquals(9, files.size) 65 | } 66 | 67 | @Test 68 | fun getDirs() = runTest { 69 | val files = tryGlob("./**/innerDir*", implicitDescendants = false) 70 | assertEquals(4, files.size) 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/httpclient/RequestHandler.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.httpclient 2 | 3 | import com.rnett.action.JsObject 4 | import internal.httpclient.HttpClientResponse 5 | import internal.httpclient.RequestInfo 6 | import kotlinx.coroutines.DelicateCoroutinesApi 7 | import kotlinx.coroutines.GlobalScope 8 | import kotlinx.coroutines.promise 9 | import kotlinx.js.set 10 | import node.ReadableStream 11 | import node.http.ClientRequestArgs 12 | import node.http.OutgoingHttpHeaders 13 | import node.http.RequestOptions 14 | import org.w3c.dom.url.URL 15 | import kotlin.js.Promise 16 | 17 | 18 | /** 19 | * A handler that modifies outgoing requests and optionally handles authentication on 401s. 20 | */ 21 | public fun interface RequestHandler { 22 | 23 | /** 24 | * Modify an outgoing request. 25 | */ 26 | public fun ClientRequestArgs.prepareRequest(headers: MutableHeaders) 27 | 28 | /** 29 | * If this returns `true`, [handleAuthentication] must be implemented. 30 | */ 31 | public fun canHandleAuthentication(response: HttpResponse): Boolean { 32 | return false 33 | } 34 | 35 | /** 36 | * Will not be called unless [canHandleAuthentication] is `true`. 37 | * 38 | * [data] will be [ReadableStream] or [String]. 39 | */ 40 | public suspend fun handleAuthentication( 41 | client: HttpClient, 42 | url: URL, 43 | data: Any?, 44 | options: ClientRequestArgs 45 | ): HttpResponse { 46 | error("Can not handle authentication") 47 | } 48 | } 49 | 50 | @OptIn(DelicateCoroutinesApi::class) 51 | internal fun RequestHandler.toInternal() = object : internal.httpclient.RequestHandler { 52 | override fun prepareRequest(options: RequestOptions) { 53 | options.prepareRequest(WrappedClientRequestArgs(options)) 54 | } 55 | 56 | override fun canHandleAuthentication(response: HttpClientResponse): Boolean { 57 | return this@toInternal.canHandleAuthentication(HttpResponseImpl(response)) 58 | } 59 | 60 | override fun handleAuthentication( 61 | httpClient: internal.httpclient.HttpClient, 62 | requestInfo: RequestInfo, 63 | data: Any? 64 | ): Promise { 65 | return GlobalScope.promise { 66 | this@toInternal.handleAuthentication( 67 | WrappedClient(httpClient), 68 | requestInfo.parsedUrl, 69 | data, 70 | requestInfo.options 71 | ).toInternal() 72 | } 73 | } 74 | 75 | } 76 | 77 | internal fun Map.toIHeaders(): OutgoingHttpHeaders = JsObject { 78 | this@toIHeaders.forEach { 79 | this[it.key] = it.value 80 | } 81 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/exec/interfaces.module_@actions_exec.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | @file:JsModule("@actions/exec") 8 | @file:JsNonModule 9 | 10 | package internal.exec 11 | 12 | import node.buffer.Buffer 13 | import node.stream.Writable 14 | 15 | 16 | internal external interface EnvBuilder { 17 | @nativeGetter 18 | operator fun get(key: String): String? 19 | 20 | @nativeSetter 21 | operator fun set(key: String, value: String) 22 | } 23 | 24 | internal external interface ExecListeners { 25 | var stdout: ((data: Buffer) -> Unit)? 26 | get() = definedExternally 27 | set(value) = definedExternally 28 | var stderr: ((data: Buffer) -> Unit)? 29 | get() = definedExternally 30 | set(value) = definedExternally 31 | var stdline: ((data: String) -> Unit)? 32 | get() = definedExternally 33 | set(value) = definedExternally 34 | var errline: ((data: String) -> Unit)? 35 | get() = definedExternally 36 | set(value) = definedExternally 37 | var debug: ((data: String) -> Unit)? 38 | get() = definedExternally 39 | set(value) = definedExternally 40 | } 41 | 42 | internal external interface ExecOptions { 43 | var cwd: String? 44 | get() = definedExternally 45 | set(value) = definedExternally 46 | var env: EnvBuilder? 47 | get() = definedExternally 48 | set(value) = definedExternally 49 | var silent: Boolean? 50 | get() = definedExternally 51 | set(value) = definedExternally 52 | var outStream: Writable? 53 | get() = definedExternally 54 | set(value) = definedExternally 55 | var errStream: Writable? 56 | get() = definedExternally 57 | set(value) = definedExternally 58 | var windowsVerbatimArguments: Boolean? 59 | get() = definedExternally 60 | set(value) = definedExternally 61 | var failOnStdErr: Boolean? 62 | get() = definedExternally 63 | set(value) = definedExternally 64 | var ignoreReturnCode: Boolean? 65 | get() = definedExternally 66 | set(value) = definedExternally 67 | var delay: Number? 68 | get() = definedExternally 69 | set(value) = definedExternally 70 | var input: Buffer? 71 | get() = definedExternally 72 | set(value) = definedExternally 73 | var listeners: ExecListeners? 74 | get() = definedExternally 75 | set(value) = definedExternally 76 | } 77 | 78 | internal external interface ExecOutput { 79 | var exitCode: Number 80 | var stdout: String 81 | var stderr: String 82 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/core/state.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.core 2 | 3 | import com.rnett.action.delegates.MutableDelegatable 4 | import com.rnett.action.delegates.ifNull 5 | import kotlin.properties.ReadOnlyProperty 6 | import kotlin.properties.ReadWriteProperty 7 | import kotlin.reflect.KProperty 8 | 9 | /** 10 | * Accessors for state. State is action-specific, and only useful when set in one phase and read in another (i.e. post). 11 | * Delegating from [state] treats the input as required. 12 | */ 13 | public object state : MutableDelegatable(), ReadWriteProperty { 14 | /** 15 | * Get state [name], returning `null` if it is not set. 16 | */ 17 | public operator fun get(name: String): String? = getOptional(name) 18 | 19 | /** 20 | * Get state [name], throwing if it is not set. 21 | */ 22 | public override fun getRequired(name: String): String = core.getRequiredState(name) 23 | 24 | /** 25 | * Get state [name], returning `null` if it is not set. 26 | */ 27 | override fun getOptional(name: String): String? = core.getState(name) 28 | 29 | /** 30 | * Set state [name] to [value]. 31 | */ 32 | public override operator fun set(name: String, value: String): Unit = core.saveState(name, value) 33 | 34 | /** 35 | * A delegate based on the property name, for a required state. 36 | */ 37 | override fun getValue(thisRef: Any?, property: KProperty<*>): String { 38 | return getRequired(property.name.delegateName()) 39 | } 40 | 41 | /** 42 | * A delegate based on the property name, for a required state. 43 | */ 44 | override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { 45 | set(property.name.delegateName(), value) 46 | } 47 | 48 | /** 49 | * Get a delegate for [name]. 50 | */ 51 | public operator fun invoke(name: String): ReadOnlyProperty = delegate(name) 52 | 53 | /** 54 | * Get an optional delegate. 55 | */ 56 | public val optional: ReadOnlyProperty by lazy { optionalDelegate(null) } 57 | 58 | /** 59 | * Get an optional delegate for [name]. 60 | */ 61 | public fun optional(name: String): ReadOnlyProperty = optionalDelegate(name) 62 | 63 | /** 64 | * Get an optional delegate with a default value. 65 | */ 66 | public fun optionalWithDefault(default: () -> String): ReadOnlyProperty = 67 | inputs.optional.ifNull(default) 68 | 69 | /** 70 | * Get an optional delegate with a default value for [name]. 71 | */ 72 | public fun optionalWithDefault(name: String, default: () -> String): ReadOnlyProperty = 73 | inputs.optional( 74 | name 75 | ).ifNull(default) 76 | } -------------------------------------------------------------------------------- /test-action/src/main/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import com.rnett.action.core.SummaryTableCell 2 | import com.rnett.action.core.env 3 | import com.rnett.action.core.inputs 4 | import com.rnett.action.core.runAction 5 | import com.rnett.action.core.summary 6 | import com.rnett.action.delegates.lines 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertFails 9 | import kotlin.test.assertNull 10 | 11 | const val requiredInputKey = "required-input" 12 | const val requiredInputValue = "required-test" 13 | 14 | const val optionalNoDefaultKey = "optional-no-default" 15 | 16 | const val withDefaultKey = "with-default" 17 | const val withDefaultValue = "Test" 18 | 19 | const val multilineKey = "multiline" 20 | val multilineValue = listOf("test1", "test2") 21 | 22 | const val testEnvKey = "test-env" 23 | const val testEnvValue = "test2" 24 | 25 | const val noEnvKey = "no-env" 26 | 27 | suspend fun main() = runAction { 28 | assertEquals(requiredInputValue, inputs[requiredInputKey]) 29 | assertEquals(requiredInputValue, inputs.getRequired(requiredInputKey)) 30 | assertEquals(requiredInputValue, inputs.getOptional(requiredInputKey)) 31 | val requiredInput by inputs 32 | assertEquals(requiredInputValue, requiredInput) 33 | 34 | assertFails { inputs.getRequired(optionalNoDefaultKey) } 35 | assertNull(inputs.getOptional(optionalNoDefaultKey)) 36 | val optionalNoDefault by inputs.optional 37 | assertNull(optionalNoDefault) 38 | 39 | assertEquals(withDefaultValue, inputs[withDefaultKey]) 40 | assertEquals(withDefaultValue, inputs.getRequired(withDefaultKey)) 41 | assertEquals(withDefaultValue, inputs.getOptional(withDefaultKey)) 42 | val withDefault by inputs 43 | assertEquals(withDefaultValue, withDefault) 44 | 45 | assertEquals(multilineValue, inputs[multilineKey]?.split("\n")) 46 | assertEquals(multilineValue, inputs.getRequired(multilineKey).split("\n")) 47 | assertEquals(multilineValue, inputs.getOptional(multilineKey)?.split("\n")) 48 | val multiline by inputs.lines() 49 | assertEquals(multilineValue, multiline) 50 | 51 | assertEquals(testEnvValue, env[testEnvKey]) 52 | assertEquals(testEnvValue, env.getRequired(testEnvKey)) 53 | val testEnv by env(testEnvKey) 54 | assertEquals(testEnvValue, testEnv) 55 | 56 | assertNull(env[noEnvKey]) 57 | assertFails { env.getRequired(noEnvKey) } 58 | var noEnv by env(noEnvKey) 59 | assertNull(noEnv) 60 | noEnv = "test" 61 | assertEquals("test", noEnv) 62 | assertEquals("test", env.getRequired(noEnvKey)) 63 | 64 | summary.append { 65 | h2("Test") 66 | hr() 67 | addTable( 68 | listOf(SummaryTableCell("Test", true), SummaryTableCell("Test2", true)), 69 | listOf(SummaryTableCell("1"), SummaryTableCell("2")) 70 | ) 71 | } 72 | 73 | println("Tests done!") 74 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/core/TopLevel.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.core 2 | 3 | import com.rnett.action.currentProcess 4 | import com.rnett.action.delegates.ifNull 5 | import com.rnett.action.delegates.isTrue 6 | import kotlin.contracts.InvocationKind 7 | import kotlin.contracts.contract 8 | 9 | /** 10 | * Mask [this] in log output. 11 | */ 12 | public fun String.maskSecret(): Unit = maskSecret(this) 13 | 14 | /** 15 | * Mask [secret] in log output. 16 | */ 17 | public fun maskSecret(secret: String): Unit = core.setSecret(secret) 18 | 19 | 20 | /** 21 | * Fail with [message], logging [message] as an error and killing the process. 22 | */ 23 | @Suppress("UNREACHABLE_CODE") 24 | public fun fail(message: String): Nothing { 25 | core.setFailed(message) 26 | currentProcess.exit(1) 27 | return error("") 28 | } 29 | 30 | /** 31 | * Fail with [exception], logging [exception] as an error and killing the process. 32 | */ 33 | @Suppress("UNREACHABLE_CODE") 34 | public fun fail(exception: Throwable): Nothing { 35 | core.setFailed(exception) 36 | exception.printStackTrace() 37 | currentProcess.exit(1) 38 | return error("") 39 | } 40 | 41 | /** 42 | * Runs [block], [fail]-ing if an exception is thrown and not caught within [block]. 43 | * 44 | * Runs [finally] in the try-catch's `finally` block. If [flush] is `true`, prints a newline in the finally block. 45 | */ 46 | public inline fun runOrFail(finally: () -> Unit = {}, flush: Boolean = true, block: () -> R): R { 47 | contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } 48 | try { 49 | return block() 50 | } catch (e: Throwable) { 51 | fail(e) 52 | } finally { 53 | finally() 54 | if (flush) 55 | println() 56 | } 57 | } 58 | 59 | /** 60 | * Runs [block], failing the action if exceptions are thrown 61 | */ 62 | public inline fun runAction(flush: Boolean = true, block: () -> Unit) { 63 | contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } 64 | runOrFail(flush = flush, block = block) 65 | } 66 | 67 | /** 68 | * Runs [block], logging (as [logger.error]) any exceptions that are not caught within [block]. 69 | * 70 | * Runs [finally] in the try-catch's `finally` block. If [flush] is `true`, prints a newline in the finally block. 71 | */ 72 | public inline fun runOrLogException(finally: () -> Unit = {}, flush: Boolean = true, block: () -> R): Result { 73 | contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } 74 | return try { 75 | Result.success(block()) 76 | } catch (e: Throwable) { 77 | logger.error(e) 78 | Result.failure(e) 79 | } finally { 80 | finally() 81 | if (flush) 82 | println() 83 | } 84 | } 85 | 86 | /** 87 | * `true` if the current environment is a GitHub actions runner. 88 | */ 89 | public val isActionRunner: Boolean by env("GITHUB_ACTIONS").isTrue().ifNull { false } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/github/interfaces.module_@actions_github.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | @file:JsModule("@actions/github") 3 | @file:JsNonModule 4 | 5 | package internal.github 6 | 7 | import kotlin.js.* 8 | 9 | internal external interface `T$13` { 10 | @nativeGetter 11 | operator fun get(key: String): Any? 12 | @nativeSetter 13 | operator fun set(key: String, value: Any) 14 | var login: String 15 | var name: String? 16 | get() = definedExternally 17 | set(value) = definedExternally 18 | } 19 | 20 | internal external interface PayloadRepository { 21 | @nativeGetter 22 | operator fun get(key: String): Any? 23 | @nativeSetter 24 | operator fun set(key: String, value: Any) 25 | var full_name: String? 26 | get() = definedExternally 27 | set(value) = definedExternally 28 | var name: String 29 | var owner: `T$13` 30 | var html_url: String? 31 | get() = definedExternally 32 | set(value) = definedExternally 33 | } 34 | 35 | internal external interface `T$14` { 36 | @nativeGetter 37 | operator fun get(key: String): Any? 38 | @nativeSetter 39 | operator fun set(key: String, value: Any) 40 | var number: Number 41 | var html_url: String? 42 | get() = definedExternally 43 | set(value) = definedExternally 44 | var body: String? 45 | get() = definedExternally 46 | set(value) = definedExternally 47 | } 48 | 49 | internal external interface `T$15` { 50 | @nativeGetter 51 | operator fun get(key: String): Any? 52 | @nativeSetter 53 | operator fun set(key: String, value: Any) 54 | var type: String 55 | } 56 | 57 | internal external interface `T$16` { 58 | var id: Number 59 | @nativeGetter 60 | operator fun get(key: String): Any? 61 | @nativeSetter 62 | operator fun set(key: String, value: Any) 63 | } 64 | 65 | internal external interface WebhookPayload { 66 | @nativeGetter 67 | operator fun get(key: String): Any? 68 | @nativeSetter 69 | operator fun set(key: String, value: Any) 70 | var repository: PayloadRepository? 71 | get() = definedExternally 72 | set(value) = definedExternally 73 | var issue: `T$14`? 74 | get() = definedExternally 75 | set(value) = definedExternally 76 | var pull_request: `T$14`? 77 | get() = definedExternally 78 | set(value) = definedExternally 79 | var sender: `T$15`? 80 | get() = definedExternally 81 | set(value) = definedExternally 82 | var action: String? 83 | get() = definedExternally 84 | set(value) = definedExternally 85 | var installation: `T$16`? 86 | get() = definedExternally 87 | set(value) = definedExternally 88 | var comment: `T$16`? 89 | get() = definedExternally 90 | set(value) = definedExternally 91 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import node.WritableStream 4 | import node.buffer.Buffer 5 | import node.process.Process 6 | import node.stream.Duplex 7 | import org.khronos.webgl.ArrayBuffer 8 | import org.khronos.webgl.Int8Array 9 | import org.khronos.webgl.Uint8Array 10 | import kotlin.contracts.InvocationKind 11 | import kotlin.contracts.contract 12 | 13 | /** 14 | * Convert a camelCase string to snake-case. 15 | */ 16 | public fun String.camelToSnakeCase(): String = replace(Regex("[^A-Z][A-Z]")) { 17 | it.value[0] + "-" + it.value[1].lowercaseChar() 18 | } 19 | 20 | /** 21 | * Convert a snake-case string to camelCase. 22 | */ 23 | public fun String.snakeToCamelCase(): String = replace(Regex("-[a-z]")) { it.value[1].uppercaseChar().toString() } 24 | 25 | 26 | /** 27 | * Create a JavaScript object for the given interface. 28 | */ 29 | public inline fun JsObject(block: T.() -> Unit = {}): T { 30 | contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } 31 | node.os.arch() 32 | val value = js("{}") as T 33 | value.block() 34 | return value 35 | } 36 | 37 | /** 38 | * Get the entries of a JS object, using `Object.entries`. 39 | */ 40 | @Suppress("NOTHING_TO_INLINE") 41 | public inline fun jsEntries(jsObject: dynamic): Map = js("Object") 42 | .entries(jsObject) 43 | .unsafeCast>>() 44 | .map { 45 | it[0].unsafeCast() to it[1] 46 | }.toMap() 47 | 48 | /** 49 | * Get the current process. Alias for [process] that doesn't have name conflicts. 50 | */ 51 | public val currentProcess: Process get() = node.process.process 52 | 53 | /** 54 | * Write a line to [this], using the OS's line seperator. 55 | */ 56 | public fun WritableStream.writeLine(buffer: String): Boolean = write(buffer + OperatingSystem.lineSeparator) 57 | 58 | /** 59 | * Write a line to [this], using the OS's line seperator. 60 | */ 61 | public fun Duplex.writeLine(buffer: String): Boolean = write(buffer + OperatingSystem.lineSeparator) 62 | 63 | /** 64 | * Non-copying conversion to a Kotlin [ByteArray]. 65 | */ 66 | public fun ArrayBuffer.asByteArray(byteOffset: Int = 0, length: Int = this.byteLength): ByteArray = 67 | Int8Array(this, byteOffset, length).asByteArray() 68 | 69 | /** 70 | * Non-copying conversion to a Kotlin [ByteArray]. 71 | */ 72 | public fun Int8Array.asByteArray(): ByteArray = this.unsafeCast() 73 | 74 | /** 75 | * Non-copying conversion to a Kotlin [ByteArray]. 76 | */ 77 | public fun Uint8Array.asByteArray(): ByteArray = Int8Array(buffer, byteOffset, length).asByteArray() 78 | 79 | /** 80 | * Non-copying conversion to a [Int8Array]. 81 | */ 82 | public fun ByteArray.asInt8Array(): Int8Array = this.unsafeCast() 83 | 84 | /** 85 | * Non-copying conversion to a [Buffer]. 86 | */ 87 | public fun ByteArray.asBuffer(): Buffer = this.asInt8Array().let { 88 | Buffer(Uint8Array(it.buffer, it.byteOffset, it.length)) 89 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/core/summary.module_@actions_core.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS") 2 | @file:JsModule("@actions/core") 3 | @file:JsNonModule 4 | 5 | package internal.core 6 | 7 | import org.khronos.webgl.* 8 | import org.w3c.dom.* 9 | import org.w3c.dom.events.* 10 | import org.w3c.dom.parsing.* 11 | import org.w3c.dom.svg.* 12 | import org.w3c.dom.url.* 13 | import org.w3c.fetch.* 14 | import org.w3c.files.* 15 | import org.w3c.notifications.* 16 | import org.w3c.performance.* 17 | import org.w3c.workers.* 18 | import org.w3c.xhr.* 19 | import kotlin.js.* 20 | 21 | internal external val summary: Summary 22 | 23 | internal external interface SummaryTableCell { 24 | var data: String 25 | var header: Boolean? 26 | get() = definedExternally 27 | set(value) = definedExternally 28 | var colspan: String? 29 | get() = definedExternally 30 | set(value) = definedExternally 31 | var rowspan: String? 32 | get() = definedExternally 33 | set(value) = definedExternally 34 | } 35 | 36 | internal external interface SummaryImageOptions { 37 | var width: String? 38 | get() = definedExternally 39 | set(value) = definedExternally 40 | var height: String? 41 | get() = definedExternally 42 | set(value) = definedExternally 43 | } 44 | 45 | internal external interface SummaryWriteOptions { 46 | var overwrite: Boolean? 47 | get() = definedExternally 48 | set(value) = definedExternally 49 | } 50 | 51 | internal external open class Summary { 52 | open var _buffer: Any 53 | open var _filePath: Any 54 | open var filePath: Any 55 | open var wrap: Any 56 | open fun write(options: SummaryWriteOptions = definedExternally): Promise 57 | open fun clear(): Promise 58 | open fun stringify(): String 59 | open fun isEmptyBuffer(): Boolean 60 | open fun emptyBuffer(): Summary 61 | open fun addRaw(text: String, addEOL: Boolean = definedExternally): Summary 62 | open fun addEOL(): Summary 63 | open fun addCodeBlock(code: String, lang: String = definedExternally): Summary 64 | open fun addList(items: Array, ordered: Boolean = definedExternally): Summary 65 | open fun addTable(rows: Array): Summary 66 | open fun addDetails(label: String, content: String): Summary 67 | open fun addImage(src: String, alt: String, options: SummaryImageOptions = definedExternally): Summary 68 | open fun addHeading(text: String, level: Number = definedExternally): Summary 69 | open fun addHeading(text: String): Summary 70 | open fun addHeading(text: String, level: String = definedExternally): Summary 71 | open fun addSeparator(): Summary 72 | open fun addBreak(): Summary 73 | open fun addQuote(text: String, cite: String = definedExternally): Summary 74 | open fun addLink(text: String, href: String): Summary 75 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/toolcache/toolcache.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | @file:JsModule("@actions/tool-cache") 8 | @file:JsNonModule 9 | 10 | package internal.toolcache 11 | 12 | import node.http.OutgoingHttpHeaders 13 | import kotlin.js.Promise 14 | 15 | internal external fun downloadTool( 16 | url: String, 17 | dest: String? = definedExternally, 18 | auth: String? = definedExternally, 19 | headers: OutgoingHttpHeaders? = definedExternally 20 | ): Promise 21 | 22 | internal external fun extract7z( 23 | file: String, 24 | dest: String? = definedExternally, 25 | _7zPath: String? = definedExternally, 26 | ): Promise 27 | 28 | internal external fun extractTar( 29 | file: String, 30 | dest: String? = definedExternally, 31 | flags: Array = definedExternally, 32 | ): Promise 33 | 34 | internal external fun extractXar( 35 | file: String, 36 | dest: String? = definedExternally, 37 | flags: Array = definedExternally, 38 | ): Promise 39 | 40 | internal external fun extractZip( 41 | file: String, 42 | dest: String? = definedExternally, 43 | ): Promise 44 | 45 | internal external fun cacheDir(sourceDir: String, tool: String, version: String, arch: String? = definedExternally): Promise 46 | 47 | internal external fun cacheFile( 48 | sourceFile: String, 49 | targetFile: String, 50 | tool: String, 51 | version: String, 52 | arch: String? = definedExternally 53 | ): Promise 54 | 55 | internal external fun find(toolName: String, versionSpec: String, arch: String? = definedExternally): String 56 | 57 | internal external fun findAllVersions(toolName: String, arch: String? = definedExternally): Array 58 | 59 | public external interface IToolRelease { 60 | public val version: String 61 | public val stable: Boolean 62 | 63 | @JsName("release_url") 64 | public val releaseUrl: String 65 | public val files: Array 66 | } 67 | 68 | public external interface IToolReleaseFile { 69 | public val filename: String 70 | public val platform: String 71 | 72 | @JsName("platform_version") 73 | public val platformVersion: String? 74 | public val arch: String 75 | 76 | @JsName("download_url") 77 | public val downloadUrl: String 78 | } 79 | 80 | internal external fun getManifestFromRepo( 81 | owner: String, 82 | repo: String, 83 | auth: String? = definedExternally, 84 | branch: String = definedExternally 85 | ): Promise> 86 | 87 | internal external fun findFromManifest( 88 | versionSpec: String, 89 | stable: Boolean, 90 | manifest: Array, 91 | archFilter: String = definedExternally 92 | ): Promise 93 | 94 | internal external fun isExplicitVersion(versionSpec: String): Boolean 95 | 96 | 97 | internal external fun evaluateVersions(versions: Array, versionSpec: String): String -------------------------------------------------------------------------------- /test-action/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /kotlin-js-action/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.apache.commons.lang3.SystemUtils 2 | import org.jetbrains.kotlin.cli.common.toBooleanLenient 3 | import org.jetbrains.kotlin.gradle.targets.js.npm.NpmDependency 4 | import java.net.URL 5 | 6 | plugins { 7 | alias(libs.plugins.kotlin.js) 8 | id("kjs-action.js-module") 9 | } 10 | 11 | description = "Utilities for writing Kotlin JS GitHub actions, including wrappers around @actions/toolkit" 12 | metadata { 13 | title.set("Kotlin JS GitHub Action SDK") 14 | } 15 | 16 | val generateExternals = false 17 | 18 | private val latestVersionRegex = Regex("\"dist-tags\":\\{\"latest\":\"([^\"]+)\"") 19 | 20 | fun DependencyHandlerScope.latestNpm( 21 | name: String, 22 | version: String, 23 | generate: Boolean = generateExternals 24 | ): NpmDependency { 25 | val url = "https://registry.npmjs.org/$name/" 26 | val latest = latestVersionRegex.find(URL(url).readText())?.groupValues?.get(1) ?: error("Version not found in $url") 27 | 28 | if (latest != version) { 29 | val message = "Using old version of npm library $name: Using $version, but latest was $latest" 30 | if ((findProperty("enforceLatest")?.toString()?.toLowerCase() ?: "false") != "false") 31 | error(message) 32 | logger.warn(message) 33 | } 34 | return npm(name, version, generate) 35 | } 36 | 37 | dependencies { 38 | testImplementation(kotlin("test")) 39 | testImplementation(libs.kotlinx.coroutines.test) 40 | testImplementation(libs.kotlinx.serialization.json) 41 | 42 | api(libs.kotlinx.coroutines.core) 43 | api(libs.kotlin.wrappers.node) 44 | 45 | implementation(latestNpm("@actions/core", "1.10.0")) 46 | implementation(latestNpm("@actions/exec", "1.1.1")) 47 | implementation(latestNpm("@actions/glob", "0.3.0")) 48 | implementation(latestNpm("@actions/io", "1.1.2")) 49 | //TODO breaks dukat 50 | implementation(latestNpm("@actions/tool-cache", "2.0.1", false)) 51 | implementation(latestNpm("@actions/github", "5.1.1")) 52 | implementation(latestNpm("@actions/artifact", "1.1.0")) 53 | implementation(latestNpm("@actions/cache", "3.0.6")) 54 | implementation(latestNpm("@actions/http-client", "2.0.1")) 55 | } 56 | 57 | 58 | 59 | kotlin { 60 | js(IR) { 61 | nodejs { 62 | testTask { 63 | val dir = layout.buildDirectory.dir("testdir").get().asFile 64 | doFirst { 65 | dir.deleteRecursively() 66 | dir.mkdirs() 67 | } 68 | doLast { 69 | dir.deleteRecursively() 70 | } 71 | environment("TEST_ENV_tempDir", dir.absolutePath) 72 | 73 | if (System.getenv("CI").toBooleanLenient() != true) { 74 | environment("TEST_ENV_projectDirPath", rootProject.layout.projectDirectory.asFile.parentFile.absolutePath) 75 | environment( 76 | "TEST_ENV_os", when { 77 | SystemUtils.IS_OS_MAC -> "mac" 78 | SystemUtils.IS_OS_LINUX -> "linux" 79 | SystemUtils.IS_OS_WINDOWS -> "windows" 80 | else -> error("Unsupported operating system") 81 | } 82 | ) 83 | environment( 84 | "TEST_ENV_userHome", 85 | File(System.getProperty("user.home")).canonicalFile.absolutePath 86 | ) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/core/core.module_@actions_core.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | @file:JsModule("@actions/core") 8 | @file:JsNonModule 9 | 10 | package internal.core 11 | 12 | import kotlin.js.Promise 13 | 14 | internal external interface InputOptions { 15 | var required: Boolean? 16 | get() = definedExternally 17 | set(value) = definedExternally 18 | var trimWhitespace: Boolean? 19 | get() = definedExternally 20 | set(value) = definedExternally 21 | } 22 | 23 | internal external enum class ExitCode { 24 | Success /* = 0 */, 25 | Failure /* = 1 */ 26 | } 27 | 28 | internal external interface AnnotationProperties { 29 | var title: String? 30 | get() = definedExternally 31 | set(value) = definedExternally 32 | var startLine: Number? 33 | get() = definedExternally 34 | set(value) = definedExternally 35 | var endLine: Number? 36 | get() = definedExternally 37 | set(value) = definedExternally 38 | var startColumn: Number? 39 | get() = definedExternally 40 | set(value) = definedExternally 41 | var endColumn: Number? 42 | get() = definedExternally 43 | set(value) = definedExternally 44 | var file: String? 45 | get() = definedExternally 46 | set(value) = definedExternally 47 | } 48 | 49 | internal external fun exportVariable(name: String, param_val: Any) 50 | 51 | internal external fun setSecret(secret: String) 52 | 53 | internal external fun addPath(inputPath: String) 54 | 55 | internal external fun getInput(name: String, options: InputOptions = definedExternally): String 56 | 57 | internal external fun getMultilineInput(name: String, options: InputOptions = definedExternally): Array 58 | 59 | internal external fun getBooleanInput(name: String, options: InputOptions = definedExternally): Boolean 60 | 61 | internal external fun setOutput(name: String, value: Any) 62 | 63 | internal external fun setCommandEcho(enabled: Boolean) 64 | 65 | internal external fun setFailed(message: String) 66 | 67 | internal external fun setFailed(message: Throwable) 68 | 69 | internal external fun isDebug(): Boolean 70 | 71 | internal external fun debug(message: String) 72 | 73 | internal external fun error(message: String, properties: AnnotationProperties = definedExternally) 74 | 75 | internal external fun error(message: String) 76 | 77 | internal external fun error(message: Throwable, properties: AnnotationProperties = definedExternally) 78 | 79 | internal external fun error(message: Throwable) 80 | 81 | internal external fun warning(message: String, properties: AnnotationProperties = definedExternally) 82 | 83 | internal external fun warning(message: String) 84 | 85 | internal external fun warning(message: Throwable, properties: AnnotationProperties = definedExternally) 86 | 87 | internal external fun warning(message: Throwable) 88 | 89 | internal external fun notice(message: String, properties: AnnotationProperties = definedExternally) 90 | 91 | internal external fun notice(message: String) 92 | 93 | internal external fun notice(message: Throwable, properties: AnnotationProperties = definedExternally) 94 | 95 | internal external fun notice(message: Throwable) 96 | 97 | internal external fun info(message: String) 98 | 99 | internal external fun startGroup(name: String) 100 | 101 | internal external fun endGroup() 102 | 103 | internal external fun group(name: String, fn: () -> Promise): Promise 104 | 105 | internal external fun saveState(name: String, value: Any) 106 | 107 | internal external fun getState(name: String): String 108 | 109 | internal external fun getIDToken(aud: String?): Promise -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/io/io.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.io 2 | 3 | import com.rnett.action.JsObject 4 | import com.rnett.action.Path 5 | import kotlinx.coroutines.await 6 | 7 | /** 8 | * Wrappers for [`@actions/io`](https://github.com/actions/toolkit/tree/main/packages/io). 9 | */ 10 | public object io { 11 | 12 | /** 13 | * Copy files. 14 | */ 15 | public suspend fun cp( 16 | source: String, 17 | dest: String, 18 | recursive: Boolean = false, 19 | force: Boolean = true, 20 | copySourceDirectory: Boolean = true 21 | ) { 22 | internal.io.cp(source, dest, JsObject { 23 | this.recursive = recursive 24 | this.force = force 25 | this.copySourceDirectory = copySourceDirectory 26 | }).await() 27 | } 28 | 29 | /** 30 | * Move files. 31 | */ 32 | public suspend fun mv(source: String, dest: String, force: Boolean = true) { 33 | internal.io.mv(source, dest, JsObject { 34 | this.force = force 35 | }).await() 36 | } 37 | 38 | /** 39 | * Remove files recursively (`rm -rf`). 40 | */ 41 | public suspend fun rmRF(inputPath: String) { 42 | internal.io.rmRF(inputPath).await() 43 | } 44 | 45 | /** 46 | * Create a directory and any parents that don't exist. 47 | */ 48 | public suspend fun mkdirP(path: String) { 49 | internal.io.mkdirP(path).await() 50 | } 51 | 52 | /** 53 | * Get the location of a tool, or `null` if it can't be found. 54 | */ 55 | public suspend fun which(tool: String): String? { 56 | return try { 57 | internal.io.which(tool, check = true).await() 58 | } catch (error: Throwable) { 59 | if (error.message != null && "Unable to locate executable file" in error.message!!) 60 | null 61 | else 62 | throw error 63 | } 64 | } 65 | 66 | /** 67 | * Copy files. 68 | */ 69 | 70 | public suspend fun cp( 71 | source: Path, dest: Path, recursive: Boolean = false, force: Boolean = true, 72 | copySourceDirectory: Boolean = true 73 | ): Unit = 74 | cp(source.path, dest.path, recursive, force, copySourceDirectory) 75 | 76 | /** 77 | * Move files. 78 | */ 79 | public suspend fun mv(source: Path, dest: Path, force: Boolean = true): Unit = mv(source.path, dest.path, force) 80 | 81 | /** 82 | * Copy files. 83 | */ 84 | public suspend fun cp( 85 | source: Path, dest: String, recursive: Boolean = false, force: Boolean = true, 86 | copySourceDirectory: Boolean = true 87 | ): Unit = 88 | cp(source.path, dest, recursive, force, copySourceDirectory) 89 | 90 | /** 91 | * Move files. 92 | */ 93 | public suspend fun mv(source: Path, dest: String, force: Boolean = true): Unit = mv(source.path, dest, force) 94 | 95 | /** 96 | * Copy files. 97 | */ 98 | public suspend fun cp( 99 | source: String, dest: Path, recursive: Boolean = false, force: Boolean = true, 100 | copySourceDirectory: Boolean = true 101 | ): Unit = 102 | cp(source, dest.path, recursive, force, copySourceDirectory) 103 | 104 | /** 105 | * Move files. 106 | */ 107 | public suspend fun mv(source: String, dest: Path, force: Boolean = true): Unit = mv(source, dest.path, force) 108 | 109 | /** 110 | * Remove files recursively (`rm -rf`). 111 | */ 112 | public suspend fun rmRF(inputPath: Path): Unit = rmRF(inputPath.path) 113 | 114 | /** 115 | * Create a directory and any parents that don't exist. 116 | */ 117 | public suspend fun mkdirP(path: Path): Unit = mkdirP(path.path) 118 | 119 | /** 120 | * Get the location of a tool, or `null` if it can't be found. 121 | */ 122 | public suspend fun which(tool: Path): Path? = which(tool.path)?.let { Path(it) } 123 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action-plugin/src/main/kotlin/com/rnett/action/AutoBuildWorkflow.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.Task 5 | import java.io.File 6 | 7 | /** 8 | * Create a task that calls [generateAutoBuildWorkflow]. 9 | * 10 | * @param add the arguments to `git add`. `dist` by default. 11 | * @param message the commit message. 12 | * @param javaVersion the java version to use in the workflow (necessary for gradle). 13 | * @param gradleTasks the gradle command line. Called like `./gradlew $gradleTasks`. 14 | * @param runner the github actions runner to use. 15 | * @param dir the directory to create `.github/workflows/auto_build_action.yml` in. `rootDir` by default. 16 | */ 17 | fun Project.autoBuildWorkflowTask( 18 | add: String = "dist", 19 | message: String = "Commit new $add", 20 | gradleTasks: String = "build", 21 | javaVersion: String = "15", 22 | runner: String = "ubuntu-latest", 23 | dir: File = rootDir, 24 | configure: Task.() -> Unit = {} 25 | ) = tasks.register("generateAutoBuildWorkflow") { 26 | doLast { 27 | generateAutoBuildWorkflow(dir, add, message, gradleTasks, javaVersion, runner) 28 | } 29 | configure() 30 | } 31 | 32 | /** 33 | * Generate sa GitHub action workflow to build (with `gradlew assemble`) and commit `dist/` on each push, if the updates one wasn't build locally. 34 | * 35 | * Generates the workflow file in `$rootDir/.github/workflows/` by default. 36 | * 37 | * @param add the arguments to `git add`. `dist` by default. 38 | * @param message the commit message. 39 | * @param javaVersion the java version to use in the workflow (necessary for gradle). 40 | * @param gradleTasks the gradle command line. Called like `./gradlew $gradleTasks`. 41 | * @param runner the github actions runner to use. 42 | * @param dir the directory to create `.github/workflows/auto_build_action.yml` in. `rootDir` by default. 43 | */ 44 | fun Project.generateAutoBuildWorkflow( 45 | add: String = "dist", 46 | message: String = "Commit new $add", 47 | gradleTasks: String = "build", 48 | javaVersion: String = "15", 49 | runner: String = "ubuntu-latest", 50 | dir: File = rootDir 51 | ) { 52 | generateAutoBuildWorkflow(dir, add, message, gradleTasks, javaVersion, runner) 53 | } 54 | 55 | /** 56 | * Generate a GitHub action workflow to build (with `gradlew assemble`) and commit `dist/` on each push, if the updates one wasn't build locally. 57 | * 58 | * @param dir the directory to create `.github/workflows/auto_build_action.yml` in. 59 | * @param add the arguments to `git add`. `dist` by default. 60 | * @param message the commit message. 61 | * @param javaVersion the java version to use in the workflow (necessary for gradle). 62 | * @param gradleTasks the gradle command line. Called like `./gradlew $gradleTasks`. 63 | * @param runner the github actions runner to use. 64 | */ 65 | fun generateAutoBuildWorkflow( 66 | dir: File, 67 | add: String = "dist", 68 | message: String = "Commit new $add", 69 | gradleTasks: String = "build", 70 | javaVersion: String = "15", 71 | runner: String = "ubuntu-latest" 72 | ) { 73 | val file = File("$dir/.github/workflows/auto_build_action.yml") 74 | file.parentFile.apply { 75 | if (!exists()) 76 | mkdirs() 77 | } 78 | 79 | file.writeText( 80 | // language=yml 81 | """ 82 | name: Auto-update dist 83 | 84 | on: 85 | push: 86 | branches: [ main, master ] 87 | pull_request: 88 | branches: [ main, master ] 89 | 90 | jobs: 91 | build: 92 | name: Update dist 93 | runs-on: $runner 94 | 95 | steps: 96 | - uses: actions/checkout@v2 97 | 98 | - name: Set up JDK 99 | uses: actions/setup-java@v1 100 | with: 101 | java-version: $javaVersion 102 | 103 | - name: Grant execute permission for gradlew 104 | run: chmod +x gradlew 105 | 106 | - name: Build 107 | run: ./gradlew $gradleTasks 108 | 109 | - name: Commit dist 110 | uses: EndBug/add-and-commit@v7 111 | with: 112 | add: '$add' 113 | message: '$message' 114 | """.trimIndent() 115 | ) 116 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | PRs are welcome! Issues are also welcome. 4 | 5 | If you're making an issue, linking to your action and a failing run will vastly speed up support. 6 | 7 | I'll make a decent effort to keep things up to date, but it's not a priority for me. 8 | 9 | ## Contributing tips 10 | 11 | ### IDE experience 12 | 13 | The IDE experience is not great at the moment. 14 | IntelliJ and Kotlin's support of Gradle composite builds is somewhat lacking, which results in the configuration import for both 15 | `kotlin-js-action` and `test-action` running at the same time and conflicting. 16 | To avoid this, import them one at a time from the Gradle tool window. 17 | 18 | Source set settings (`explicitApi` mode, module-wide `@OptIn`s) sometimes aren't picked up. 19 | The Gradle tasks are your source of truth, not necessary the IDE. 20 | 21 | ### Testing 22 | 23 | While it never hurts to test locally, the GitHub actions CI that runs for each pull request is good enough to rely on. 24 | The e2e tests are only possible there. 25 | If you need permissions to run it, feel free to @ me. 26 | 27 | If you want to run the tests locally, run `test` in `kotlin-js-action` (make sure it's in `kotlin-js-action`, IntelliJ's Gradle command runner 28 | will often use `test-action`, which will run no tests). 29 | 30 | ### Project structure 31 | 32 | ``` 33 | kotlin-js-action/ A parent for the library elements. A Gradle root project. 34 | kotlin-js-action/ The core JS library. 35 | serialization/ Extensions for kotlin-js-action that use Kotlinx.serialization. 36 | kotlin-js-action-plugin/ A Gradle plugin to make configuring your Kotlin/JS build to produce a GitHub action executable easy. 37 | 38 | test-action/ A GitHub action that uses this library, for e2e testing. A Gradle root project. 39 | ``` 40 | 41 | `test-action` consumes `kotlin-js-action` (the parent) as an included build. 42 | 43 | The core of this library is `kotlin-js-action`. 44 | It is primarily wrappers around GitHub's `@actions/toolkit` JS packages. 45 | 46 | There are also some utilities for working with NodeJS types using idiomatic Kotlin (focused around `Path`s and streams for now). 47 | Additional utilities are always welcome, and a good way to get started. 48 | 49 | ### Updating `@actions/toolkit` libraries 50 | 51 | The process for updating `@actions/toolkit` is surprisingly painless, mostly because they are fairly stable. 52 | 53 | 1. In `./kotlin-js-action`, run `./gradlew help`. You will see output like: 54 | 55 | ``` 56 | Using old version of npm library @actions/core: Using 1.6.0, but latest was 1.10.0 57 | Using old version of npm library @actions/exec: Using 1.1.0, but latest was 1.1.1 58 | Using old version of npm library @actions/glob: Using 0.2.0, but latest was 0.3.0 59 | Using old version of npm library @actions/io: Using 1.1.1, but latest was 1.1.2 60 | Using old version of npm library @actions/tool-cache: Using 1.7.1, but latest was 2.0.1 61 | Using old version of npm library @actions/github: Using 5.0.0, but latest was 5.1.1 62 | Using old version of npm library @actions/artifact: Using 0.6.1, but latest was 1.1.0 63 | Using old version of npm library @actions/cache: Using 1.0.8, but latest was 3.0.6 64 | Using old version of npm library @actions/http-client: Using 1.0.11, but latest was 2.0.1 65 | ``` 66 | 67 | 2. Go to the [`@actions/toolkit`](https://github.com/actions/toolkit) page for each of the out of date library. 68 | Check the changelog (the `RELEASES.md` for each library, some are in reverse order). 69 | 3. For each library that looks like it has breaking changes, update the version in `kotlin-js-action/kotlin-js-action/build.gradle.kts`, 70 | comment out the `implementation(latestNpm("@actions/tool-cache", "1.7.1", false))` line with the `//TODO breaks dukat` comment, and run 71 | `generateExternals` (in `kotlin-js-action`). 72 | 4. This will generate a bunch of external JS definitions in `kotlin-js-action/kotlin-js-action/externals`. 73 | Look for any with your newly updated library's name in the filename. 74 | Either add them to the appropriate package in `kotlin-js-action/kotlin-js-action/src/main/kotlin/internal` (if they are new) 75 | or diff them with the existing definitions and update as necessary. 76 | You will need to add `internal` to all of the `external` definitions. 77 | 5. Then update the nice wrapper definitions in `com.rnett.action`. 78 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/core/logger.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.core 2 | 3 | import kotlin.contracts.InvocationKind 4 | import kotlin.contracts.contract 5 | 6 | 7 | /** 8 | * Methods to log to the GitHub action log. 9 | * 10 | * Coloring output is supported via [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). 11 | * 3/4 bit, 8 bit and 24 bit colors are all supported. 12 | * 13 | * See [https://github.com/actions/toolkit/tree/main/packages/core#styling-output](https://github.com/actions/toolkit/tree/main/packages/core#styling-output) 14 | */ 15 | public object logger { 16 | 17 | /** 18 | * Get whether the action is running in debug mode. 19 | */ 20 | public val isDebug: Boolean get() = core.isDebug 21 | 22 | /** 23 | * Setter to set whether commands are echoed to the log. 24 | * 25 | * Getter always returns `null`. 26 | */ 27 | public var echoCommands: Boolean? by core::echoCommands 28 | 29 | /** 30 | * Log a debug message. 31 | */ 32 | public fun debug(message: String): Unit = core.debug(message) 33 | 34 | /** 35 | * Log an info message. 36 | */ 37 | public fun info(message: String): Unit = core.info(message) 38 | 39 | /** 40 | * Log a notice message. 41 | */ 42 | public fun notice(message: String): Unit = core.notice(message) 43 | 44 | /** 45 | * Log [exception] as a notice message. 46 | */ 47 | public fun notice(exception: Throwable): Unit = core.notice(exception) 48 | 49 | /** 50 | * Log a notice message. 51 | */ 52 | public fun notice(message: String, annotationProperties: AnnotationProperties): Unit = 53 | core.notice(message, annotationProperties) 54 | 55 | /** 56 | * Log [exception] as a notice message. 57 | */ 58 | public fun notice(exception: Throwable, annotationProperties: AnnotationProperties): Unit = 59 | core.notice(exception, annotationProperties) 60 | 61 | /** 62 | * Log a warning message. 63 | */ 64 | public fun warning(message: String): Unit = core.warning(message) 65 | 66 | /** 67 | * Log [exception] as a warning message. 68 | */ 69 | public fun warning(exception: Throwable): Unit = core.warning(exception) 70 | 71 | /** 72 | * Log a warning message. 73 | */ 74 | public fun warning(message: String, annotationProperties: AnnotationProperties): Unit = 75 | core.warning(message, annotationProperties) 76 | 77 | /** 78 | * Log [exception] as a warning message. 79 | */ 80 | public fun warning(exception: Throwable, annotationProperties: AnnotationProperties): Unit = 81 | core.warning(exception, annotationProperties) 82 | 83 | /** 84 | * Log an error message. 85 | */ 86 | public fun error(message: String): Unit = core.error(message) 87 | 88 | /** 89 | * Log [exception] as an error message. 90 | */ 91 | public fun error(exception: Throwable): Unit = core.error(exception) 92 | 93 | /** 94 | * Log an error message. 95 | */ 96 | public fun error(message: String, annotationProperties: AnnotationProperties): Unit = 97 | core.error(message, annotationProperties) 98 | 99 | /** 100 | * Log [exception] as an error message. 101 | */ 102 | public fun error(exception: Throwable, annotationProperties: AnnotationProperties): Unit = 103 | core.error(exception, annotationProperties) 104 | 105 | /** 106 | * Log a fatal message. This logs the message as an error and then sets failure. 107 | * 108 | * Does not exit the process, for that use [fail]. 109 | */ 110 | public fun fatal(message: String): Unit = core.setFailed(message) 111 | 112 | 113 | /** 114 | * Log [exception] as a fatal message. This logs the message as an error and then sets failure. 115 | * 116 | * Does not exit the process, for that use [fail]. 117 | */ 118 | public fun fatal(exception: Throwable): Unit = core.setFailed(exception) 119 | 120 | /** 121 | * Executes [block] within a fordable output group of [name]. 122 | */ 123 | public inline fun withGroup(name: String, block: () -> R): R { 124 | contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } 125 | return core.withGroup(name, block) 126 | } 127 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Next 4 | 5 | ### Breaking 6 | 7 | ### Non-breaking 8 | 9 | ## 1.6.0 10 | 11 | ### Breaking 12 | 13 | * Update to Node 16.18.0 14 | * Switch from the obsolete `kotlinx-nodejs` 15 | to [`kotlin-wrappers/node`](https://github.com/JetBrains/kotlin-wrappers/blob/master/kotlin-node/README.md). 16 | **This is a massively breaking change!**. 17 | * Numerous APIs have changed as a result of this, and many NodeJS types have changed. 18 | * This includes `Path`, which has changed significantly. There are no more non-`suspend` methods, and many properties became `suspend` methods. 19 | * It also gets rid of the `jcenter()` requirement :tada: 20 | * Update Kotlin to 1.7.21 21 | * Update Kotlinx.serialization to 1.4.1 22 | * Update Kotlinx.coroutines to 1.6.4 23 | * Update `@actions/http-client` to `2.0.1` 24 | 25 | ### Non-breaking 26 | 27 | Note: Many of these supposedly non-breaking updates have had breaking changes in the same areas, due to the NodeJS wrapper change. 28 | 29 | * Update `@actions/core` to `1.10.0`. This adds summary creation, accessible via the `summary` object. 30 | * Update `@actions/exec` to `1.1.1`. 31 | * Update `@actions/glob` to `0.3.0`. 32 | * Update `@actions/io` to `1.1.2`. 33 | * Update `@actions/tool-cache` to `2.0.1`. 34 | * Update `@actions/github` to `5.1.1`. 35 | * Update `@actions/artifact` to `1.1.0`. 36 | * Update `@actions/cache` to `3.0.6`. 37 | * Update `@actions/http-client` to `2.0.1`. 38 | 39 | ## 1.5.0 40 | 41 | ### Breaking 42 | 43 | * Update Kotlin to 1.6.10 44 | * Update Kotlinx coroutines to 1.6.0 45 | * Update Kotlinx serialization to 1.3.2 46 | 47 | ### Non-breaking 48 | 49 | * Update `@actions/artifact` to 0.6.1 50 | 51 | ## 1.4.3 52 | 53 | ### Non-breaking 54 | 55 | * Update `@actions/core` to 1.5.0. 56 | * Add `notice` logging methods. 57 | * Add `AnnotationProperties` and annotation producing overloads for `notice`, `warning`, and `error`. 58 | 59 | ## 1.4.2 60 | 61 | ### Non-breaking 62 | 63 | * Update Kotlin to 1.5.30. 64 | 65 | ## 1.4.1 66 | 67 | ### Non-breaking 68 | 69 | * Update serialization to `1.2.2` and coroutines to `1.5.1`. 70 | * Add wrappers for `@actions/tool-cache` 71 | * Add `Path.deleteRecursively()`. 72 | * Add `PATH += Path`. 73 | 74 | ## 1.4.0 75 | 76 | ### Slightly breaking 77 | 78 | * Restructure HttpClient types. Some classes were replaced by interfaces or typealiases, but breakage should be pretty 79 | small unless you used the Json client or the legacy json request methods. 80 | * Make the auto-build workflow use the `build` task, by default. 81 | 82 | ### Non-breaking 83 | 84 | * Add a plugin method to add a task to generate the auto-build workflow. 85 | * Builtin withDefault delegate methods 86 | 87 | ## 1.3.0 88 | 89 | Many breaking changes, mostly around `env`, `Path`, `inputs`, and `state`. The whole library has been cleaned up and 90 | tested. 91 | 92 | ### Breaking 93 | 94 | * **Made `execCapture` methods error on a command error return by default**. 95 | * Cache client is properly marked as experimental 96 | * `core.getInput` is removed in favor of explicit `getRequiredInput` or `getOptionalInput`. 97 | * `inputs`, `env`/`exportEnv`, and `state` had their delegation reworked. Should be compatible for basic use cases 98 | except `env`, where the delegates became optional by default. 99 | * Lots of type conversion methods were added for the reworked delegates 100 | * `log` was renamed to `logger`. 101 | * Updated underlying `exec` bindings, used new capture method. 102 | * Fixed `execShell` shell escaping. 103 | * Removed `hashFiles` from `github.context`, added a wrapper for the underlying method to the `glob` package. 104 | * Updated the underlying `glob` package, added `matchDirectories` option and `hashFiles`. 105 | * Removed `currentOS` and `lineSeperator` in favor of `OperatingSystem.current`/`.lineSeperator`, added and moved other 106 | properties to `OperatingSystem` companion. 107 | * Made `Path.cwd` settable (it calls `cd`). 108 | * Make `Path` read, write, and append methods `suspend` by default, added non-suspending versions. 109 | * Move `Path.seperator` to `OperatingSystem.pathSeperator`. 110 | * Reworked the `Path.copy` and `Path.move` methods, seperate out `*Into` and `*ChildrenInto`. 111 | * Replace `Path.read` with `Path.readText`, add `Path.readBytes`. 112 | 113 | ### Non-breaking 114 | 115 | * Add stream utils 116 | * Add Buffer <-> ByteArray conversions 117 | * Add tests 118 | * Added wrappers for `@actions/http-client`. Used via `HttpClient` or `JsonHttpClient` in the `serialization` artifact. 119 | * Add serialization artifact for Kotlinx serialization support 120 | * Added `runAction`, like `runOrFail` but returns `Unit`. 121 | * Add `Buffer` and `ByteArray` `Path` write methods. 122 | -------------------------------------------------------------------------------- /kotlin-js-action/serialization/src/main/kotlin/com/rnett/action/serialization/HttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.serialization 2 | 3 | import com.rnett.action.httpclient.BaseHttpClient 4 | import com.rnett.action.httpclient.HeaderProvider 5 | import com.rnett.action.httpclient.HttpClient 6 | import com.rnett.action.httpclient.HttpClientBuilder 7 | import com.rnett.action.httpclient.HttpResponse 8 | import kotlinx.coroutines.coroutineScope 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.map 11 | import kotlinx.serialization.decodeFromString 12 | import kotlinx.serialization.encodeToString 13 | import kotlinx.serialization.json.Json 14 | import node.ReadableStream 15 | import kotlin.contracts.InvocationKind 16 | import kotlin.contracts.contract 17 | 18 | /** 19 | * A HTTP response that you can get JSON deserialized responses from. 20 | */ 21 | public class JsonHttpResponse(response: HttpResponse, @PublishedApi internal val json: Json) : 22 | HttpResponse by response { 23 | public suspend inline fun readJsonBody(): T = json.decodeFromString(readBody()) 24 | } 25 | 26 | /** 27 | * Get a Http client with json support wrapping this client. 28 | * 29 | * Closing the returned client will not close the wrapped client. 30 | */ 31 | public fun HttpClient.json(json: Json = Json): JsonHttpClient = if (this is JsonHttpClient && this.json == json) 32 | this 33 | else 34 | JsonHttpClient(json, this, false) 35 | 36 | /** 37 | * A [`@actions/http-client`](https://github.com/actions/http-client) based Http client that uses 38 | * kotlinx serialization Json parsing and adds `Accept` (always) and `Content-Type` (*Json methods) headers. 39 | */ 40 | public inline fun JsonHttpClient(json: Json = Json, builder: HttpClientBuilder.() -> Unit = {}): JsonHttpClient { 41 | contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } 42 | return JsonHttpClient( 43 | json, 44 | HttpClient { 45 | builder() 46 | addHeader("accept", "application/json") 47 | } 48 | ) 49 | } 50 | 51 | /** 52 | * A [`@actions/http-client`](https://github.com/actions/http-client) based Http client that uses 53 | * kotlinx serialization Json parsing and adds `Accept` (always) and `Content-Type` (*Json methods) headers. 54 | */ 55 | public class JsonHttpClient @PublishedApi internal constructor( 56 | @PublishedApi internal val json: Json, 57 | private val client: HttpClient, 58 | private val closeClient: Boolean = true 59 | ) : 60 | BaseHttpClient { 61 | 62 | override suspend fun request(verb: String, url: String, data: String, headers: HeaderProvider): JsonHttpResponse = 63 | JsonHttpResponse(client.request(verb, url, data, headers), json) 64 | 65 | override suspend fun request( 66 | verb: String, 67 | url: String, 68 | data: ReadableStream, 69 | headers: HeaderProvider 70 | ): JsonHttpResponse = JsonHttpResponse(client.request(verb, url, data, headers), json) 71 | 72 | public suspend inline fun postJson( 73 | url: String, 74 | data: T, 75 | headers: HeaderProvider = HeaderProvider { } 76 | ): JsonHttpResponse = 77 | requestJson("post", url, data, headers = headers) 78 | 79 | public suspend inline fun postJson( 80 | url: String, 81 | data: Flow, 82 | headers: HeaderProvider = HeaderProvider { } 83 | ): JsonHttpResponse = 84 | requestJson("post", url, data, headers = headers) 85 | 86 | public suspend inline fun putJson( 87 | url: String, 88 | data: T, 89 | headers: HeaderProvider = HeaderProvider { } 90 | ): JsonHttpResponse = 91 | requestJson("put", url, data, headers = headers) 92 | 93 | public suspend inline fun putJson( 94 | url: String, 95 | data: Flow, 96 | headers: HeaderProvider = HeaderProvider { } 97 | ): JsonHttpResponse = 98 | requestJson("put", url, data, headers = headers) 99 | 100 | public suspend inline fun patchJson( 101 | url: String, 102 | data: T, 103 | headers: HeaderProvider = HeaderProvider { } 104 | ): JsonHttpResponse = 105 | requestJson("patch", url, data, headers = headers) 106 | 107 | public suspend inline fun patchJson( 108 | url: String, 109 | data: Flow, 110 | headers: HeaderProvider = HeaderProvider { } 111 | ): JsonHttpResponse = 112 | requestJson("patch", url, data, headers = headers) 113 | 114 | public suspend inline fun requestJson( 115 | verb: String, 116 | url: String, 117 | data: T, 118 | headers: HeaderProvider = HeaderProvider {} 119 | ): JsonHttpResponse { 120 | val response = 121 | request(verb, url, json.encodeToString(data), headers + { add("content-type", "application/json") }) 122 | return JsonHttpResponse(response, json) 123 | } 124 | 125 | public suspend inline fun requestJson( 126 | verb: String, 127 | url: String, 128 | data: Flow, 129 | headers: HeaderProvider = HeaderProvider {} 130 | ): JsonHttpResponse = coroutineScope { 131 | val response = request( 132 | verb, 133 | url, 134 | data.map { json.encodeToString(it) }, 135 | headers + { add("content-type", "application/json") }) 136 | JsonHttpResponse(response, json) 137 | } 138 | 139 | override fun close() { 140 | if (closeClient) 141 | client.close() 142 | } 143 | } -------------------------------------------------------------------------------- /kotlin-js-action/serialization/src/main/kotlin/com/rnett/action/serialization/ReadOnlyDelegates.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.serialization 2 | 3 | import com.rnett.action.delegates.map 4 | import com.rnett.action.delegates.mapNonNull 5 | import kotlinx.serialization.BinaryFormat 6 | import kotlinx.serialization.DeserializationStrategy 7 | import kotlinx.serialization.ExperimentalSerializationApi 8 | import kotlinx.serialization.SerializationStrategy 9 | import kotlinx.serialization.StringFormat 10 | import kotlinx.serialization.decodeFromByteArray 11 | import kotlinx.serialization.decodeFromString 12 | import kotlinx.serialization.encodeToByteArray 13 | import kotlinx.serialization.encodeToString 14 | import kotlin.properties.ReadOnlyProperty 15 | 16 | /** 17 | * Deserialize the read string. 18 | */ 19 | @ExperimentalSerializationApi 20 | public fun ReadOnlyProperty.deserialize( 21 | format: StringFormat, 22 | serializer: DeserializationStrategy 23 | ): ReadOnlyProperty = map { format.decodeFromString(serializer, it) } 24 | 25 | /** 26 | * Deserialize the read string if it is non-null. 27 | */ 28 | @ExperimentalSerializationApi 29 | public fun ReadOnlyProperty.deserializeNotNull( 30 | format: StringFormat, 31 | serializer: DeserializationStrategy 32 | ): ReadOnlyProperty = mapNonNull { format.decodeFromString(serializer, it) } 33 | 34 | /** 35 | * Serialize the read value. 36 | */ 37 | @ExperimentalSerializationApi 38 | public fun ReadOnlyProperty.serialize( 39 | format: StringFormat, 40 | serializer: SerializationStrategy 41 | ): ReadOnlyProperty = map { format.encodeToString(serializer, it) } 42 | 43 | /** 44 | * Serialize the read value, if it is non-null. 45 | */ 46 | @ExperimentalSerializationApi 47 | public fun ReadOnlyProperty.serializeNonNull( 48 | format: StringFormat, 49 | serializer: SerializationStrategy 50 | ): ReadOnlyProperty = mapNonNull { format.encodeToString(serializer, it) } 51 | 52 | /** 53 | * Deserialize the read string. 54 | */ 55 | @ExperimentalSerializationApi 56 | public fun ReadOnlyProperty.deserialize( 57 | format: BinaryFormat, 58 | serializer: DeserializationStrategy 59 | ): ReadOnlyProperty = map { format.decodeFromByteArray(serializer, it) } 60 | 61 | /** 62 | * Deserialize the read string if it is non-null. 63 | */ 64 | @ExperimentalSerializationApi 65 | public fun ReadOnlyProperty.deserializeNotNull( 66 | format: BinaryFormat, 67 | serializer: DeserializationStrategy 68 | ): ReadOnlyProperty = mapNonNull { format.decodeFromByteArray(serializer, it) } 69 | 70 | /** 71 | * Serialize the read value. 72 | */ 73 | @ExperimentalSerializationApi 74 | public fun ReadOnlyProperty.serialize( 75 | format: BinaryFormat, 76 | serializer: SerializationStrategy 77 | ): ReadOnlyProperty = map { format.encodeToByteArray(serializer, it) } 78 | 79 | /** 80 | * Serialize the read value, if it is non-null. 81 | */ 82 | @ExperimentalSerializationApi 83 | public fun ReadOnlyProperty.serializeNonNull( 84 | format: BinaryFormat, 85 | serializer: SerializationStrategy 86 | ): ReadOnlyProperty = mapNonNull { format.encodeToByteArray(serializer, it) } 87 | 88 | /** 89 | * Deserialize the read string. 90 | */ 91 | @ExperimentalSerializationApi 92 | public inline fun ReadOnlyProperty.deserialize( 93 | format: StringFormat 94 | ): ReadOnlyProperty = map { format.decodeFromString(it) } 95 | 96 | /** 97 | * Deserialize the read string if it is non-null. 98 | */ 99 | @ExperimentalSerializationApi 100 | public inline fun ReadOnlyProperty.deserializeNotNull( 101 | format: StringFormat 102 | ): ReadOnlyProperty = mapNonNull { format.decodeFromString(it) } 103 | 104 | /** 105 | * Serialize the read value. 106 | */ 107 | @ExperimentalSerializationApi 108 | public inline fun ReadOnlyProperty.serialize( 109 | format: StringFormat 110 | ): ReadOnlyProperty = map { format.encodeToString(it) } 111 | 112 | /** 113 | * Serialize the read value, if it is non-null. 114 | */ 115 | @ExperimentalSerializationApi 116 | public inline fun ReadOnlyProperty.serializeNonNull( 117 | format: StringFormat 118 | ): ReadOnlyProperty = mapNonNull { format.encodeToString(it) } 119 | 120 | /** 121 | * Deserialize the read string. 122 | */ 123 | @ExperimentalSerializationApi 124 | public inline fun ReadOnlyProperty.deserialize( 125 | format: BinaryFormat 126 | ): ReadOnlyProperty = map { format.decodeFromByteArray(it) } 127 | 128 | /** 129 | * Deserialize the read string if it is non-null. 130 | */ 131 | @ExperimentalSerializationApi 132 | public inline fun ReadOnlyProperty.deserializeNotNull( 133 | format: BinaryFormat 134 | ): ReadOnlyProperty = mapNonNull { format.decodeFromByteArray(it) } 135 | 136 | /** 137 | * Serialize the read value. 138 | */ 139 | @ExperimentalSerializationApi 140 | public inline fun ReadOnlyProperty.serialize( 141 | format: BinaryFormat 142 | ): ReadOnlyProperty = map { format.encodeToByteArray(it) } 143 | 144 | /** 145 | * Serialize the read value, if it is non-null. 146 | */ 147 | @ExperimentalSerializationApi 148 | public inline fun ReadOnlyProperty.serializeNonNull( 149 | format: BinaryFormat 150 | ): ReadOnlyProperty = mapNonNull { format.encodeToByteArray(it) } 151 | 152 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/core/Env.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.core 2 | 3 | import com.rnett.action.currentProcess 4 | import com.rnett.action.delegates.MutableDelegatable 5 | import com.rnett.action.delegates.ifNull 6 | import kotlinx.js.delete 7 | import kotlinx.js.get 8 | import kotlinx.js.set 9 | import kotlin.properties.ReadOnlyProperty 10 | import kotlin.properties.ReadWriteProperty 11 | import kotlin.reflect.KProperty 12 | 13 | /** 14 | * Environment variable accessors, that by default exports if [defaultExport] is `true`. 15 | * 16 | * Can be delegated from. 17 | * Delegating from [Environment] treats the input as optional. 18 | */ 19 | public abstract class Environment(private val defaultExport: Boolean) : MutableDelegatable(), 20 | ReadWriteProperty { 21 | 22 | /** 23 | * Get [name] from the environment, or throw if it is not present. 24 | */ 25 | public override fun getRequired(name: String): String = get(name) ?: error("No environment variable $name") 26 | 27 | /** 28 | * Get [name] from the environment, or `null` if it is not present. 29 | */ 30 | override fun getOptional(name: String): String? = currentProcess.env[name] 31 | 32 | /** 33 | * Get [name] from the environment, or `null` if it is not present. 34 | */ 35 | public operator fun get(name: String): String? = getOptional(name) 36 | 37 | /** 38 | * Get [name] from the environment, or set [default] for [name] and return it. 39 | * 40 | * Follows the [Environment]'s export setting by default. 41 | */ 42 | public fun getOrPut(name: String, export: Boolean, default: () -> String): String = 43 | get(name) ?: default().also { 44 | set(name, export, it) 45 | } 46 | 47 | /** 48 | * Set [name] in the environment, following the default export setting for the [Environment]. 49 | */ 50 | public override operator fun set(name: String, value: String) { 51 | set(name, defaultExport, value) 52 | } 53 | 54 | /** 55 | * Set [name] in the environment, following the default export setting for the [Environment]. 56 | */ 57 | public operator fun set(name: String, value: String?) { 58 | set(name, defaultExport, value) 59 | } 60 | 61 | /** 62 | * Set [name] in the environment, exporting according to [export]. 63 | */ 64 | public operator fun set(name: String, export: Boolean, value: String?) { 65 | if (export && value != null) 66 | core.exportVariable(name, value) 67 | if (value != null) { 68 | currentProcess.env[name] = value 69 | } else { 70 | delete(currentProcess.env.asDynamic()[name]) 71 | } 72 | } 73 | 74 | /** 75 | * Remove [name] from the environment. Does not affect exports. 76 | */ 77 | public override fun remove(name: String) { 78 | delete(currentProcess.env.asDynamic()[name]) 79 | } 80 | 81 | /** 82 | * Export [name] with [value]. Alias for `set(name, true, value)`. 83 | */ 84 | public fun export(name: String, value: String) { 85 | set(name, true, value) 86 | } 87 | 88 | /** 89 | * Export [name] with it's current value. Does not export if it isn't set. 90 | */ 91 | public fun export(name: String) { 92 | val value = this[name] 93 | if (value != null) { 94 | export(name, value) 95 | } 96 | } 97 | 98 | private val selfDelegate by lazy { optionalDelegate(null) } 99 | 100 | /** 101 | * A delegate based on the property name, for an optional state. 102 | */ 103 | override fun getValue(thisRef: Any?, property: KProperty<*>): String? { 104 | return selfDelegate.getValue(thisRef, property) 105 | } 106 | 107 | /** 108 | * A delegate based on the property name, for an optional state. 109 | */ 110 | public override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) { 111 | selfDelegate.setValue(thisRef, property, value) 112 | } 113 | 114 | /** 115 | * Delegate for [name]. Follows the the [Environment]'s export setting. 116 | */ 117 | public operator fun invoke(name: String): ReadWriteProperty = optionalDelegate(name) 118 | 119 | /** 120 | * Optional delegate. Follows the the [Environment]'s export setting. 121 | */ 122 | public val required: ReadWriteProperty by lazy { delegate(null) } 123 | 124 | /** 125 | * Optional delegate for [name]. Follows the the [Environment]'s export setting. 126 | */ 127 | public fun required(name: String): ReadWriteProperty = delegate(name) 128 | 129 | /** 130 | * Get an optional delegate with a default value. 131 | */ 132 | public fun withDefault(default: () -> String): ReadOnlyProperty = inputs.optional.ifNull(default) 133 | 134 | /** 135 | * Get an optional delegate with a default value for [name]. 136 | */ 137 | public fun withDefault(name: String, default: () -> String): ReadOnlyProperty = inputs.optional( 138 | name 139 | ).ifNull(default) 140 | } 141 | 142 | /** 143 | * Environment accessors, where set variables are exported for other steps and tasks. 144 | * 145 | * Export setting can be overridden in most methods, but will export by default. 146 | */ 147 | public object exportEnv : Environment(true) { 148 | /** 149 | * Get [env] 150 | */ 151 | public val local: env = env 152 | } 153 | 154 | /** 155 | * Environment accessors, where set variables are **not** exported (i.e. not available in other steps and tasks). 156 | * 157 | * Export setting can be overridden in most methods, but will not export by default. 158 | */ 159 | public object env : Environment(false) { 160 | /** 161 | * Get [exportEnv] 162 | */ 163 | public val export: exportEnv = exportEnv 164 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/artifact/artifact.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.artifact 2 | 3 | import com.rnett.action.JsObject 4 | import com.rnett.action.Path 5 | import kotlinx.coroutines.await 6 | 7 | /** 8 | * True if some items in the upload failed. 9 | */ 10 | public val UploadResponse.hasFailedItems: Boolean get() = failedItems.isNotEmpty() 11 | 12 | /** 13 | * Throws an error if the upload has failed items. 14 | */ 15 | public fun UploadResponse.requireSuccess() { 16 | if (hasFailedItems) 17 | error("Upload had failed items: $failedItems") 18 | } 19 | 20 | /** 21 | * The full Path to where the artifact was downloaded 22 | */ 23 | public val DownloadResponse.downloadLocation: Path get() = Path(downloadPath) 24 | 25 | /** 26 | * Wrappers for [`@actions/artifact`](https://github.com/actions/toolkit/tree/main/packages/artifact). Creates and uses one client. 27 | */ 28 | public object artifact { 29 | private val client by lazy { internal.artifact.create() } 30 | 31 | /** 32 | * Uploads an artifact 33 | * 34 | * @param name the name of the artifact, required 35 | * @param files a list of absolute or relative paths that denote what files should be uploaded 36 | * @param rootDirectory an absolute or relative file path that denotes the root parent directory of the files being uploaded 37 | * @param continueOnError whether the upload should continue if there is an error in a part. Failed parts will be reflected in the [UploadResponse]. 38 | * @param retentionDays How long to hold the artifact. If `null` (default), defaults to the repository setting. Can be between 1 and the repository max (which by default is 90) 39 | * @returns single UploadInfo object 40 | */ 41 | public suspend fun uploadArtifact( 42 | name: String, files: List, rootDirectory: String, continueOnError: Boolean = true, 43 | retentionDays: Int? = null 44 | ): UploadResponse { 45 | return client.uploadArtifact(name, files.toTypedArray(), rootDirectory, JsObject { 46 | if (retentionDays != null) 47 | this.retentionDays = retentionDays 48 | this.continueOnError = continueOnError 49 | }).await() 50 | } 51 | 52 | 53 | /** 54 | * Uploads an artifact 55 | * 56 | * @param name the name of the artifact, required 57 | * @param files a list of absolute or relative paths that denote what files should be uploaded 58 | * @param rootDirectory an absolute or relative file path that denotes the root parent directory of the files being uploaded 59 | * @param continueOnError whether the upload should continue if there is an error in a part. Failed parts will be reflected in the [UploadResponse]. 60 | * @param retentionDays How long to hold the artifact. If `null` (default), defaults to the repository setting. Can be between 1 and the repository max (which by default is 90) 61 | * @returns single [UploadResponse] object 62 | */ 63 | public suspend fun uploadArtifact( 64 | name: String, files: List, rootDirectory: Path, continueOnError: Boolean = true, 65 | retentionDays: Int? = null 66 | ): UploadResponse = uploadArtifact(name, files.map { it.path }, rootDirectory.path, continueOnError, retentionDays) 67 | 68 | /** 69 | * Downloads a single artifact associated with a run. Will error if it fails. 70 | * 71 | * @param name the name of the artifact being downloaded 72 | * @param path optional path that denotes where the artifact will be downloaded to 73 | * @param createArtifactFolder whether to create a folder for the artifact in [path]. Otherwise downloads the contents into [path]. 74 | */ 75 | public suspend fun downloadArtifact( 76 | name: String, 77 | path: String? = null, 78 | createArtifactFolder: Boolean = false 79 | ): DownloadResponse { 80 | return if (path == null) { 81 | client.downloadArtifact(name, options = JsObject { 82 | this.createArtifactFolder = createArtifactFolder 83 | }).await() 84 | } else { 85 | client.downloadArtifact(name, path, JsObject { 86 | this.createArtifactFolder = createArtifactFolder 87 | }).await() 88 | } 89 | } 90 | 91 | /** 92 | * Downloads all artifacts associated with a run. Will error if it fails. 93 | * Because there are multiple artifacts being downloaded, a folder will be created for each one in the specified or default directory 94 | * @param path optional path that denotes where the artifacts will be downloaded to 95 | */ 96 | public suspend fun downloadAllArtifacts(path: String? = null): List { 97 | return if (path == null) { 98 | client.downloadAllArtifacts().await().toList() 99 | } else { 100 | client.downloadAllArtifacts(path).await().toList() 101 | } 102 | } 103 | 104 | /** 105 | * Downloads a single artifact associated with a run 106 | * 107 | * @param name the name of the artifact being downloaded 108 | * @param path optional path that denotes where the artifact will be downloaded to 109 | * @param createArtifactFolder whether to create a folder for the artifact in [path]. Otherwise downloads the contents into [path]. 110 | */ 111 | public suspend fun downloadArtifact( 112 | name: String, 113 | path: Path? = null, 114 | createArtifactFolder: Boolean = false 115 | ): DownloadResponse = downloadArtifact(name, path?.path, createArtifactFolder) 116 | 117 | /** 118 | * Downloads all artifacts associated with a run. Because there are multiple artifacts being downloaded, a folder will be created for each one in the specified or default directory 119 | * @param path optional path that denotes where the artifacts will be downloaded to 120 | */ 121 | public suspend fun downloadAllArtifacts(path: Path? = null): List = 122 | downloadAllArtifacts(path?.path) 123 | 124 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin JS GitHub Action SDK 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/com.github.rnett.ktjs-github-action/kotlin-js-action)](https://search.maven.org/artifact/com.github.rnett.ktjs-github-action/kotlin-js-action) 4 | [![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/com.github.rnett.ktjs-github-action/kotlin-js-action?server=https%3A%2F%2Foss.sonatype.org)](https://oss.sonatype.org/content/repositories/snapshots/com/github/rnett/ktjs-github-action/) 5 | [![GitHub Repo](https://img.shields.io/badge/GitHub-kotlin--js--action-blue?logo=github)](https://github.com/rnett/kotlin-js-action) 6 | [![License](https://img.shields.io/badge/License-Apache%202.0-yellowgreen.svg)](https://opensource.org/licenses/Apache-2.0) 7 | [![Changelog](https://img.shields.io/badge/Changelog-CHANGELOG.md-green)](./CHANGELOG.md#changelog) 8 | 9 | ### Artifacts 10 | 11 | * `com.github.rnett.ktjs-github-action:kotlin-js-action` - the SDK 12 | * `com.github.rnett.ktjs-github-action:serialization` - Kotlinx serialization support for the SDK 13 | * `com.github.rnett.ktjs-github-action` - a gradle plugin for building GitHub Actions. The maven artifact 14 | is `com.github.rnett.ktjs-github-action:kotlin-js-action-plugin` 15 | if you are not using the `plugins` block. 16 | 17 | Using the IR backend is required. 18 | 19 | ### [Docs](https://rnett.github.io/kotlin-js-action/release/) 20 | 21 | [For latest SNAPSHOT build](https://rnett.github.io/kotlin-js-action/snapshot/) 22 | 23 | Kotlin JS utilities for writing GitHub Actions, including wrappers 24 | for [actions/toolkit](https://github.com/actions/toolkit) packages, except for `@actions/github` 25 | and `@actions/tool-cache`. `@actions/tool-cache` will be added once blocking `dukat` bugs are fixes. 26 | 27 | ## SDK 28 | 29 | The GitHub actions SDK provides wrappers for most of the [`actions/toolkit`](https://github.com/actions/toolkit) 30 | packages, as well as some utilities for working with NodeJS types. 31 | 32 | Actions should almost always have an entrypoint like: 33 | 34 | ```kotlin 35 | suspend fun main() = runAction { 36 | 37 | } 38 | ``` 39 | 40 | This ensures that any uncaught exceptions are properly reported to the GitHub runtime. 41 | 42 | Generally, most of the wrappers are thin and can be fully understood by reading the docs. We explain the more 43 | complicated ones below. 44 | 45 | ### Utils 46 | 47 | Utilities include backpressure cognizant `Flow` <-> `Stream` conversions, 48 | `WritableStream.writeSuspending` (which suspend for backpressure), 49 | `Buffer.asByteArray()`, `jsEntries`, and an `external interface` object builder `JsObject`. 50 | 51 | ### Inputs, State, and Env 52 | 53 | The `inputs`, `state`, and `env` (and `exportEnv`) objects provide action input, state, and environment accessors. They 54 | can all be accessed by key, but also provide delegates. Delegates can delegate from the object directly, specify a name 55 | with `invoke(String)`, or whether the value is required with `optional`/`optional(String)` and `required` 56 | /`required(String)`. 57 | `inputs` and `state` delegates are required by default and the objects have `optional` versions, where `env` is the 58 | opposite. 59 | 60 | `exportEnv`/`env.export` is another environment wrapper that exports set environment variables to the GitHub workflow. 61 | 62 | ### Typesafe delegates 63 | 64 | Many objects, including `inputs`, `env`, and `outputs`, are accessed primarily by `String` delegates. However, this is 65 | insufficient when accessing structured data, such as multi-line or boolean inputs or JSON formatted `state`. 66 | 67 | To this end, we provide delegate (`ReadOnlyProperty` and `ReadWriteProperty`, to be exact) mapping functions in 68 | the `delegates` package. You can write your own using `map`, `mapNonNull`, `ifNull`, etc, and we provide a large set of 69 | default implementations including `isTrue` (\~`toBoolean` in the stdlib), 70 | `toBoolean` (\~`toBooleanStrict` in the stdlib), `toInt`, `lines`, `trim`, and `lowercase` for both read-only and 71 | read-write delegates. 72 | 73 | This is also how serialization support is implemented. The `serialization` artifact provides `deserialize` 74 | and `serialize` methods, so you can do something like `val myState by state.deserialize()`. 75 | 76 | ### Shell Exec 77 | 78 | `exec` is based on the `@actions/exec` package, however it has some issues with input redirection, in particular that 79 | passing `outStream` will cause a `[command] $command` header to be written to the output file. To this end we provide 80 | `execShell` commands that instead of executing the raw command, excuting it using something like `bash -c "$command"`. 81 | Powershell (Windows) or bash are used by default. Note that powershell output redirects (`>`) will be written in 82 | UTF-16-le with a BOM. Command strings passed here support things like `>`, `|`, aliases, and posix-like powershell 83 | commands. 84 | 85 | ### HttpClient 86 | 87 | The `httpclient` package wraps `@actions/http-client`. `HttpClient` provides a lightweight http client, with the 88 | ability to send, `Flow`s and text, and set default and per-request headers. While the typed result methods are provided, 89 | typed requests should usually be handled via `JsonHttpClient` in the `serialization` artifact. 90 | 91 | ## Gradle Plugin 92 | 93 | The gradle plugin (`com.github.rnett.ktjs-github-action`) can Kotlin JS with bundling for GitHub Actions, since the 94 | Kotlin JS plugin doesn't support bundling NodeJS yet. 95 | 96 | There are two functions you can call in your build script: 97 | 98 | * `com.rnett.action.githubAction`, called in the `js` target block to configure the target for GitHub actions. It 99 | configures a browser target, but with node dependencies in Webpack, and adds a task to generate the custom webpack 100 | config. This replaces `browser` or `nodejs`. 101 | * `com.rnett.action.generateAutoBuildWorkflow`, called anywhere. Generates a GitHub actions workflow for the project to 102 | build and commit the distributable on push, in case it wasn't built locally. This keeps it up to date with your latest 103 | changes. By default, if called with a `Project` receiver, generates it in `$rootDir/.github/workflows/` 104 | 105 | ## Examples 106 | 107 | The [test action](./test-action) and its [metadata file](./.github/actions/test-action/action.yml) 108 | 109 | https://github.com/rnett/find-regex 110 | 111 | https://github.com/rnett/publish-docs 112 | 113 | https://github.com/rnett/import-gpg-key 114 | -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/internal/httpclient/index.module_@actions_http-client.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | @file:JsModule("@actions/http-client") 8 | @file:JsNonModule 9 | 10 | package internal.httpclient 11 | 12 | import node.ReadableStream 13 | import node.http.Agent 14 | import node.http.IncomingMessage 15 | import node.http.OutgoingHttpHeaders 16 | import kotlin.js.Promise 17 | 18 | internal external enum class HttpCodes { 19 | OK /* = 200 */, 20 | MultipleChoices /* = 300 */, 21 | MovedPermanently /* = 301 */, 22 | ResourceMoved /* = 302 */, 23 | SeeOther /* = 303 */, 24 | NotModified /* = 304 */, 25 | UseProxy /* = 305 */, 26 | SwitchProxy /* = 306 */, 27 | TemporaryRedirect /* = 307 */, 28 | PermanentRedirect /* = 308 */, 29 | BadRequest /* = 400 */, 30 | Unauthorized /* = 401 */, 31 | PaymentRequired /* = 402 */, 32 | Forbidden /* = 403 */, 33 | NotFound /* = 404 */, 34 | MethodNotAllowed /* = 405 */, 35 | NotAcceptable /* = 406 */, 36 | ProxyAuthenticationRequired /* = 407 */, 37 | RequestTimeout /* = 408 */, 38 | Conflict /* = 409 */, 39 | Gone /* = 410 */, 40 | TooManyRequests /* = 429 */, 41 | InternalServerError /* = 500 */, 42 | NotImplemented /* = 501 */, 43 | BadGateway /* = 502 */, 44 | ServiceUnavailable /* = 503 */, 45 | GatewayTimeout /* = 504 */ 46 | } 47 | 48 | internal external enum class Headers { 49 | Accept /* = "accept" */, 50 | ContentType /* = "content-type" */ 51 | } 52 | 53 | internal external enum class MediaTypes { 54 | ApplicationJson /* = "application/json" */ 55 | } 56 | 57 | internal external fun getProxyUrl(serverUrl: String): String 58 | 59 | internal external open class HttpClientResponse(message: IncomingMessage) { 60 | open var message: IncomingMessage 61 | open fun readBody(): Promise 62 | } 63 | 64 | internal external fun isHttps(requestUrl: String): Boolean 65 | 66 | internal open external class HttpClient( 67 | userAgent: String = definedExternally, 68 | handlers: Array = definedExternally, 69 | requestOptions: internal.httpclient.RequestOptions = definedExternally 70 | ) { 71 | open var userAgent: String? 72 | open var handlers: Array 73 | open var requestOptions: internal.httpclient.RequestOptions? 74 | open var _ignoreSslError: Any 75 | open var _socketTimeout: Any 76 | open var _allowRedirects: Any 77 | open var _allowRedirectDowngrade: Any 78 | open var _maxRedirects: Any 79 | open var _allowRetries: Any 80 | open var _maxRetries: Any 81 | open var _agent: Any 82 | open var _proxyAgent: Any 83 | open var _keepAlive: Any 84 | open var _disposed: Any 85 | open fun options( 86 | requestUrl: String, 87 | additionalHeaders: OutgoingHttpHeaders = definedExternally 88 | ): Promise 89 | 90 | open fun get( 91 | requestUrl: String, 92 | additionalHeaders: OutgoingHttpHeaders = definedExternally 93 | ): Promise 94 | 95 | open fun del( 96 | requestUrl: String, 97 | additionalHeaders: OutgoingHttpHeaders = definedExternally 98 | ): Promise 99 | 100 | open fun post( 101 | requestUrl: String, 102 | data: String, 103 | additionalHeaders: OutgoingHttpHeaders = definedExternally 104 | ): Promise 105 | 106 | open fun patch( 107 | requestUrl: String, 108 | data: String, 109 | additionalHeaders: OutgoingHttpHeaders = definedExternally 110 | ): Promise 111 | 112 | open fun put( 113 | requestUrl: String, 114 | data: String, 115 | additionalHeaders: OutgoingHttpHeaders = definedExternally 116 | ): Promise 117 | 118 | open fun head( 119 | requestUrl: String, 120 | additionalHeaders: OutgoingHttpHeaders = definedExternally 121 | ): Promise 122 | 123 | open fun sendStream( 124 | verb: String, 125 | requestUrl: String, 126 | stream: ReadableStream, 127 | additionalHeaders: OutgoingHttpHeaders = definedExternally 128 | ): Promise 129 | 130 | open fun getJson( 131 | requestUrl: String, 132 | additionalHeaders: OutgoingHttpHeaders = definedExternally 133 | ): Promise> 134 | 135 | open fun postJson( 136 | requestUrl: String, 137 | obj: Any, 138 | additionalHeaders: OutgoingHttpHeaders = definedExternally 139 | ): Promise> 140 | 141 | open fun putJson( 142 | requestUrl: String, 143 | obj: Any, 144 | additionalHeaders: OutgoingHttpHeaders = definedExternally 145 | ): Promise> 146 | 147 | open fun patchJson( 148 | requestUrl: String, 149 | obj: Any, 150 | additionalHeaders: OutgoingHttpHeaders = definedExternally 151 | ): Promise> 152 | 153 | open fun request( 154 | verb: String, 155 | requestUrl: String, 156 | data: String, 157 | headers: OutgoingHttpHeaders 158 | ): Promise 159 | 160 | open fun request( 161 | verb: String, 162 | requestUrl: String, 163 | data: ReadableStream, 164 | headers: OutgoingHttpHeaders 165 | ): Promise 166 | 167 | open fun dispose() 168 | open fun requestRaw(info: RequestInfo, data: String): Promise 169 | open fun requestRaw(info: RequestInfo, data: ReadableStream): Promise 170 | open fun requestRawWithCallback( 171 | info: RequestInfo, 172 | data: String, 173 | onResult: (err: Any, res: HttpClientResponse) -> Unit 174 | ) 175 | 176 | open fun requestRawWithCallback( 177 | info: RequestInfo, 178 | data: ReadableStream, 179 | onResult: (err: Any, res: HttpClientResponse) -> Unit 180 | ) 181 | 182 | open fun getAgent(serverUrl: String): Agent 183 | internal open var _prepareRequest: Any 184 | internal open var _mergeHeaders: Any 185 | internal open var _getExistingOrDefaultHeader: Any 186 | internal open var _getAgent: Any 187 | internal open var _performExponentialBackoff: Any 188 | internal open var _processResponse: Any 189 | } -------------------------------------------------------------------------------- /kotlin-js-action/kotlin-js-action/src/main/kotlin/com/rnett/action/httpclient/Headers.kt: -------------------------------------------------------------------------------- 1 | package com.rnett.action.httpclient 2 | 3 | import com.rnett.action.JsObject 4 | import com.rnett.action.jsEntries 5 | import kotlinx.js.get 6 | import kotlinx.js.set 7 | import node.http.ClientRequestArgs 8 | import node.http.OutgoingHttpHeaders 9 | 10 | internal fun Any?.headerToString() = when (val value = this) { 11 | null -> null 12 | is String -> value 13 | is Array<*> -> value.joinToString(",") 14 | else -> value.toString() 15 | } 16 | 17 | /** 18 | * An object or lambda that provides some headers. 19 | */ 20 | public fun interface HeaderProvider : RequestHandler { 21 | public fun MutableHeaders.headers() 22 | override fun ClientRequestArgs.prepareRequest(headers: MutableHeaders) { 23 | headers.headers() 24 | } 25 | 26 | @Suppress("REDUNDANT_LABEL_WARNING") 27 | public operator fun plus(other: HeaderProvider): HeaderProvider = function@ HeaderProvider { 28 | +this@HeaderProvider 29 | +other 30 | } 31 | } 32 | 33 | public operator fun Map.plus(header: HeaderProvider): Map = 34 | this + MapHeaders().apply { +header }.toMap() 35 | 36 | public fun HeaderProvider.toMap(): Map = MapHeaders().apply { headers() }.toMap() 37 | 38 | internal fun HeaderProvider.toOutgoingHeaders(): OutgoingHttpHeaders = 39 | JsObject().apply { WrappedOutgoingHttpHeaders(this).apply { headers() } } 40 | 41 | /** 42 | * Read-only non case sensitive HTTP headers. 43 | */ 44 | public sealed interface Headers { 45 | 46 | /** 47 | * Whether the value of [name] is set. 48 | */ 49 | public operator fun contains(name: String): Boolean = get(name) != null 50 | 51 | /** 52 | * Get the value of a header. 53 | */ 54 | public operator fun get(name: String): String? 55 | 56 | public fun toMap(): Map 57 | } 58 | 59 | /** 60 | * Mutable non case sensitive HTTP headers. 61 | */ 62 | public sealed interface MutableHeaders : Headers { 63 | 64 | /** 65 | * Set a header. 66 | */ 67 | public operator fun set(name: String, value: String) 68 | 69 | /** 70 | * Add a header, adding it to the existing header after a `,` if a this header has already been set. 71 | */ 72 | public fun add(name: String, value: String) { 73 | val current = this[name] 74 | if (current.isNullOrBlank()) { 75 | this[name] = value 76 | } else { 77 | this[name] = "$current,$value" 78 | } 79 | } 80 | 81 | /** 82 | * Add a header. 83 | * @see add 84 | */ 85 | public operator fun plusAssign(header: Pair): Unit = add(header.first, header.second) 86 | 87 | /** 88 | * Add headers from a provider. 89 | */ 90 | public operator fun plusAssign(other: HeaderProvider) { 91 | other.apply { 92 | headers() 93 | } 94 | } 95 | 96 | /** 97 | * Add headers from a provider. 98 | */ 99 | public operator fun HeaderProvider.unaryPlus() { 100 | this@MutableHeaders += this 101 | } 102 | 103 | /** 104 | * Set multiple headers. 105 | * @see set 106 | */ 107 | public infix fun setFrom(other: Map) { 108 | other.forEach { (k, v) -> this[k] = v } 109 | } 110 | 111 | /** 112 | * Add multiple headers. 113 | * @see add 114 | */ 115 | public infix fun addFrom(other: Map) { 116 | other.forEach { (k, v) -> add(k, v) } 117 | } 118 | 119 | /** 120 | * Add multiple headers. 121 | * @see add 122 | */ 123 | public operator fun plusAssign(other: Map) { 124 | addFrom(other) 125 | } 126 | 127 | /** 128 | * Add multiple headers. 129 | * @see add 130 | */ 131 | public operator fun Map.unaryPlus() { 132 | this@MutableHeaders += this 133 | } 134 | 135 | /** 136 | * Set multiple headers. 137 | * @see set 138 | */ 139 | public infix fun setFrom(other: Iterable>) { 140 | other.forEach { (k, v) -> this[k] = v } 141 | } 142 | 143 | /** 144 | * Add multiple headers. 145 | * @see add 146 | */ 147 | public infix fun addFrom(other: Iterable>) { 148 | other.forEach { (k, v) -> add(k, v) } 149 | } 150 | 151 | /** 152 | * Add multiple headers. 153 | * @see add 154 | */ 155 | public operator fun plusAssign(other: Iterable>) { 156 | addFrom(other) 157 | } 158 | 159 | /** 160 | * Add multiple headers. 161 | * @see add 162 | */ 163 | public operator fun Iterable>.unaryPlus() { 164 | this@MutableHeaders += this 165 | } 166 | } 167 | 168 | internal class MapHeaders private constructor(private val map: MutableMap = mutableMapOf()) : 169 | MutableHeaders { 170 | constructor(map: Map = emptyMap()) : this(map.mapKeysTo(mutableMapOf()) { it.key.lowercase() }) 171 | 172 | override fun get(name: String): String? = map[name.lowercase()] 173 | 174 | override fun toMap(): Map = map 175 | 176 | override fun set(name: String, value: String) { 177 | map[name.lowercase()] = value 178 | } 179 | } 180 | 181 | internal class WrappedOutgoingHttpHeaders(private val headers: OutgoingHttpHeaders) : 182 | MutableHeaders { 183 | override fun get(name: String): String? = headers[name.lowercase()].headerToString() 184 | 185 | override fun toMap(): Map = jsEntries(headers).mapNotNull { (key, value) -> 186 | value.unsafeCast().headerToString()?.let { key to it } 187 | }.toMap() 188 | 189 | override fun set(name: String, value: String) { 190 | headers[name.lowercase()] = value 191 | } 192 | } 193 | 194 | 195 | internal class WrappedClientRequestArgs(private val internal: ClientRequestArgs) : MutableHeaders { 196 | 197 | private val internalHeaders: OutgoingHttpHeaders 198 | get() { 199 | if (internal.headers == null) { 200 | internal.headers = JsObject() 201 | } 202 | return internal.headers!! 203 | } 204 | 205 | override fun get(name: String): String? = internalHeaders[name].unsafeCast().headerToString() 206 | 207 | override fun set(name: String, value: String) { 208 | internalHeaders[name.lowercase()] = value 209 | } 210 | 211 | override fun toMap(): Map = 212 | jsEntries(internalHeaders).mapNotNull { (key, value) -> 213 | value.unsafeCast().headerToString()?.let { key to it } 214 | }.toMap() 215 | } --------------------------------------------------------------------------------