├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── core-feature-request.md ├── semantic.yml └── workflows │ ├── _shared.main.kts │ ├── bindings-server.main.kts │ ├── bindings-server.yaml │ ├── build.main.kts │ ├── build.yaml │ ├── end-to-end-tests-2nd-workflow.yaml │ ├── end-to-end-tests.main.kts │ ├── end-to-end-tests.yaml │ ├── gradle-wrapper-validation.main.kts │ ├── gradle-wrapper-validation.yaml │ ├── release.main.kts │ ├── release.yaml │ ├── setup-java.main.kts │ ├── setup-python.main.kts │ ├── test-gradle-project-using-bindings-server │ ├── build.gradle.kts │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── settings.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── Main.kt │ ├── test-local-action │ └── action.yml │ ├── test-script-consuming-jit-bindings.main.do-not-compile.kts │ └── test-served-bindings-depend-on-library.main.do-not-compile.kts ├── .gitignore ├── .idea ├── externalDependencies.xml └── icon.svg ├── CONTRIBUTING.md ├── LICENSE ├── MAINTENANCE.md ├── README.md ├── action-binding-generator ├── api │ └── action-binding-generator.api ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── io │ │ └── github │ │ └── typesafegithub │ │ └── workflows │ │ └── actionbindinggenerator │ │ ├── domain │ │ ├── ActionCoords.kt │ │ ├── MetadataRevision.kt │ │ ├── SignificantVersion.kt │ │ └── TypingActualSource.kt │ │ ├── generation │ │ ├── ClassNaming.kt │ │ └── Generation.kt │ │ ├── metadata │ │ ├── InputNullability.kt │ │ └── MetadataReading.kt │ │ ├── typing │ │ ├── ActionTypes.kt │ │ ├── TypesProviding.kt │ │ ├── Typing.kt │ │ └── TypingGeneration.kt │ │ └── utils │ │ └── TextUtils.kt │ └── test │ ├── kotlin │ └── io │ │ └── github │ │ └── typesafegithub │ │ └── workflows │ │ └── actionbindinggenerator │ │ ├── Utils.kt │ │ ├── bindingsfromunittests │ │ ├── ActionWithAllTypesOfInputs.kt │ │ ├── ActionWithAllTypesOfInputsTest.kt │ │ ├── ActionWithAllTypesOfInputs_Untyped.kt │ │ ├── ActionWithDeprecatedInputAndNameClash.kt │ │ ├── ActionWithFancyCharsInDocs.kt │ │ ├── ActionWithInputsSharingType.kt │ │ ├── ActionWithNoInputs.kt │ │ ├── ActionWithNoInputsWithMajorVersion.kt │ │ ├── ActionWithNoInputsWithMajorVersion_Untyped.kt │ │ ├── ActionWithNoInputsWithMinorVersion.kt │ │ ├── ActionWithNoInputsWithMinorVersion_Untyped.kt │ │ ├── ActionWithNoTypings_Untyped.kt │ │ ├── ActionWithOutputs.kt │ │ ├── ActionWithOutputsTest.kt │ │ ├── ActionWithPartlyTypings.kt │ │ ├── ActionWithPartlyTypings_Untyped.kt │ │ ├── ActionWithSomeOptionalInputs.kt │ │ ├── ActionWithSomeOptionalInputsTest.kt │ │ ├── ActionWithSubAction.kt │ │ ├── SimpleActionWithRequiredStringInputs.kt │ │ └── SimpleActionWithRequiredStringInputsTest.kt │ │ ├── generation │ │ ├── ClassNamingTest.kt │ │ └── GenerationTest.kt │ │ ├── metadata │ │ └── InputNullabilityTest.kt │ │ ├── typing │ │ └── TypesProvidingTest.kt │ │ └── utils │ │ └── TextUtilsTest.kt │ └── resources │ └── types │ ├── action-types.yml │ └── action.yml ├── action-updates-checker ├── api │ └── action-updates-checker.api ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── io │ │ └── github │ │ └── typesafegithub │ │ └── workflows │ │ └── updates │ │ ├── GithubLogging.kt │ │ ├── Reporting.kt │ │ ├── Utils.kt │ │ └── model │ │ └── RegularActionVersions.kt │ └── test │ └── kotlin │ └── io │ └── github │ └── typesafegithub │ └── workflows │ └── updates │ └── ReportingTest.kt ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── repositories.settings.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── buildsrc │ ├── convention │ ├── duplicate-versions.gradle.kts │ ├── kotlin-jvm-server.gradle.kts │ ├── kotlin-jvm.gradle.kts │ └── publishing.gradle.kts │ └── tasks │ └── AwaitMavenCentralDeployTask.kt ├── code-generator ├── build.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── io │ │ │ └── github │ │ │ └── typesafegithub │ │ │ └── workflows │ │ │ ├── codegenerator │ │ │ ├── KspGenerator.kt │ │ │ └── KspGeneratorProvider.kt │ │ │ └── dsl │ │ │ └── expressions │ │ │ ├── GenerateEventPayloads.kt │ │ │ └── TextUtils.kt │ └── resources │ │ ├── META-INF │ │ └── services │ │ │ └── com.google.devtools.ksp.processing.SymbolProcessorProvider │ │ └── payloads │ │ ├── event-pull-request.json │ │ ├── event-push.json │ │ ├── event-release.json │ │ └── event-workflow-dispatch.json │ └── test │ └── kotlin │ └── io │ └── github │ └── typesafegithub │ └── workflows │ └── dsl │ └── expressions │ └── TextUtilsTest.kt ├── docs ├── Logo.svg ├── faq.md ├── feature-coverage.md ├── index.md ├── projects-using-this-library.md ├── requirements.txt └── user-guide │ ├── compensating-librarys-missing-features.md │ ├── getting_started.md │ ├── job-outputs.md │ ├── migrating-to-Maven-based-bindings.md │ ├── nightly-builds.md │ ├── type-safe-expressions.md │ └── using-actions.md ├── github-workflows-kt ├── api │ └── github-workflows-kt.api ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── io │ │ └── github │ │ └── typesafegithub │ │ └── workflows │ │ ├── annotations │ │ └── ExperimentalKotlinLogicStep.kt │ │ ├── domain │ │ ├── AbstractResult.kt │ │ ├── Concurrency.kt │ │ ├── Container.kt │ │ ├── Environment.kt │ │ ├── Job.kt │ │ ├── JobOutputs.kt │ │ ├── RunnerType.kt │ │ ├── Shell.kt │ │ ├── Step.kt │ │ ├── Workflow.kt │ │ ├── actions │ │ │ ├── Action.kt │ │ │ └── CustomAction.kt │ │ ├── contexts │ │ │ ├── Contexts.kt │ │ │ ├── GithubContext.kt │ │ │ └── OutputsContext.kt │ │ └── triggers │ │ │ ├── BranchProtectionRule.kt │ │ │ ├── CheckRun.kt │ │ │ ├── CheckSuite.kt │ │ │ ├── Create.kt │ │ │ ├── Delete.kt │ │ │ ├── Deployment.kt │ │ │ ├── DeploymentStatus.kt │ │ │ ├── Discussion.kt │ │ │ ├── DiscussionComment.kt │ │ │ ├── Fork.kt │ │ │ ├── Gollum.kt │ │ │ ├── IssueComment.kt │ │ │ ├── Issues.kt │ │ │ ├── Label.kt │ │ │ ├── MergeGroup.kt │ │ │ ├── Milestone.kt │ │ │ ├── PageBuild.kt │ │ │ ├── Project.kt │ │ │ ├── ProjectCard.kt │ │ │ ├── ProjectColumn.kt │ │ │ ├── PublicWorkflow.kt │ │ │ ├── PullRequest.kt │ │ │ ├── PullRequestReview.kt │ │ │ ├── PullRequestReviewComment.kt │ │ │ ├── PullRequestTarget.kt │ │ │ ├── Push.kt │ │ │ ├── RegistryPackage.kt │ │ │ ├── Release.kt │ │ │ ├── RepositoryDispatch.kt │ │ │ ├── Schedule.kt │ │ │ ├── Status.kt │ │ │ ├── Trigger.kt │ │ │ ├── Watch.kt │ │ │ ├── WorkflowCall.kt │ │ │ ├── WorkflowDispatch.kt │ │ │ └── WorkflowRun.kt │ │ ├── dsl │ │ ├── Container.kt │ │ ├── GithubActionsDsl.kt │ │ ├── HasCustomArguments.kt │ │ ├── JobBuilder.kt │ │ ├── WorkflowBuilder.kt │ │ └── expressions │ │ │ ├── Contexts.kt │ │ │ ├── Expression.kt │ │ │ ├── ExpressionContext.kt │ │ │ └── contexts │ │ │ ├── EnvContext.kt │ │ │ ├── FunctionsContext.kt │ │ │ ├── GitHubContext.kt │ │ │ ├── RunnerContext.kt │ │ │ ├── SecretsContext.kt │ │ │ └── VarsContext.kt │ │ ├── internal │ │ ├── CaseEnumSerializer.kt │ │ ├── InternalGithubActionsApi.kt │ │ └── PathUtils.kt │ │ ├── validation │ │ └── Values.kt │ │ └── yaml │ │ ├── Case.kt │ │ ├── ConsistencyCheckJob.kt │ │ ├── ConsistencyCheckJobConfig.kt │ │ ├── ContainerToYaml.kt │ │ ├── ContextMapping.kt │ │ ├── JobsToYaml.kt │ │ ├── ObjectToYaml.kt │ │ ├── Preamble.kt │ │ ├── StepsToYaml.kt │ │ ├── ToYaml.kt │ │ ├── TriggersToYaml.kt │ │ └── Utils.kt │ └── test │ ├── kotlin │ └── io │ │ └── github │ │ └── typesafegithub │ │ └── workflows │ │ ├── IntegrationTest.kt │ │ ├── NonCompilableTest.kt │ │ ├── actions │ │ └── CustomActionTest.kt │ │ ├── docsnippets │ │ ├── CompensatingLibrarysMissingFeaturesSnippets.kt │ │ ├── GettingStartedSnippets.kt │ │ ├── JobOutputsSnippets.kt │ │ ├── TypeSafeExpressionsSnippets.kt │ │ └── UsingActionsSnippets.kt │ │ ├── domain │ │ ├── ContainerTest.kt │ │ ├── JobTest.kt │ │ ├── RunnerTypeTest.kt │ │ ├── StepTest.kt │ │ └── triggers │ │ │ ├── OtherTriggersTest.kt │ │ │ ├── PullRequestTargetTest.kt │ │ │ ├── PullRequestTest.kt │ │ │ ├── PushTest.kt │ │ │ └── ScheduleTest.kt │ │ ├── dsl │ │ ├── JobBuilderTest.kt │ │ ├── WorkflowBuilderTest.kt │ │ └── expressions │ │ │ ├── ContextsTest.kt │ │ │ └── PayloadTest.kt │ │ └── yaml │ │ ├── CaseTest.kt │ │ ├── ContainerToYamlTest.kt │ │ ├── ContextMappingTest.kt │ │ ├── JobsToYamlTest.kt │ │ ├── ObjectToYamlTest.kt │ │ ├── StepsToYamlTest.kt │ │ └── TriggersToYamlTest.kt │ └── resources │ ├── contexts │ ├── github-all-fields.json │ └── github-required-fields.json │ └── payloads │ └── runner.json ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── Logo-Black.svg ├── Logo-White.svg └── teaser-with-newest-version.svg ├── jit-binding-server ├── build.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── io │ │ │ └── github │ │ │ └── typesafegithub │ │ │ └── workflows │ │ │ └── jitbindingserver │ │ │ ├── ActionCoords.kt │ │ │ ├── ArtifactRoutes.kt │ │ │ ├── InternalRoutes.kt │ │ │ ├── Main.kt │ │ │ ├── MetadataRoutes.kt │ │ │ └── Plugins.kt │ └── resources │ │ └── log4j2.xml │ └── test │ └── kotlin │ └── io │ └── github │ └── typesafegithub │ └── workflows │ └── jitbindingserver │ ├── ArtifactRoutesTest.kt │ └── MetadataRoutesTest.kt ├── maven-binding-builder ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── io │ │ └── github │ │ └── typesafegithub │ │ └── workflows │ │ └── mavenbinding │ │ ├── ActionCoordsUtils.kt │ │ ├── Checksums.kt │ │ ├── JarBuilding.kt │ │ ├── MavenMetadataBuilding.kt │ │ ├── ModuleBuilding.kt │ │ ├── PackageArtifactsBuilding.kt │ │ ├── PomBuilding.kt │ │ ├── VersionArtifactsBuilding.kt │ │ └── ZipUtils.kt │ └── test │ └── kotlin │ └── io │ └── github │ └── typesafegithub │ └── workflows │ └── mavenbinding │ └── MavenMetadataBuildingTest.kt ├── mkdocs.yml ├── renovate.json ├── settings.gradle.kts └── shared-internal ├── build.gradle.kts └── src ├── main └── kotlin │ └── io │ └── github │ └── typesafegithub │ └── workflows │ └── shared │ └── internal │ ├── GitHubApp.kt │ ├── GithubApi.kt │ ├── GithubUtils.kt │ ├── PathUtils.kt │ └── model │ └── Version.kt └── test └── kotlin └── io └── github └── typesafegithub └── workflows └── shared └── internal └── model ├── GithubApiTest.kt └── VersionTest.kt /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [{*.kt, *.kts}] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | ij_kotlin_allow_trailing_comma = true 11 | ij_kotlin_allow_trailing_comma_on_call_site = true 12 | ij_kotlin_packages_to_use_import_on_demand = unset 13 | ij_kotlin_name_count_to_use_star_import = 2147483647 14 | ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.bat text eol=crlf 4 | *.jar binary 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: krzema12 2 | custom: https://www.buymeacoffee.com/krzema12 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something isn't working properly 4 | title: "[Bug] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Component 11 | 12 | * [ ] github-workflows-kt (library with the DSL) 13 | * [ ] bindings server (https://bindings.krzeminski.it) 14 | * [ ] I don't know 15 | 16 | # Action 17 | 18 | _Describe what you are trying to do._ 19 | 20 | # Expected 21 | 22 | _What you would expect?_ 23 | 24 | # Actual 25 | 26 | _What happens instead?_ 27 | 28 | # Workaround, if exists 29 | 30 | _Manual adjustment of output YAML is not a workaround. Adjusting Kotlin code in a certain way is._ 31 | 32 | # Version 33 | 34 | _If it's a bug in the library, specify it's version._ 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/core-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Core feature request 3 | about: Request adding support for some core GitHub Actions workflows feature 4 | title: "[Core feature request] " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What feature do you need?** 11 | _Please add a link to GitHub docs (https://docs.github.com/en/actions/using-workflows/) which describes the desired feature._ 12 | 13 | **Do you have an example usage?** 14 | _Best if a YAML snippet is pasted here, or if your project is open-source, an URL to your workflow._ 15 | 16 | **Is there a workaround for not having this feature? If yes, please describe it.** 17 | _Please take a look at https://krzema12.github.io/github-workflows-kt/user-guide/compensating-librarys-missing-features/ as a mechanism designed to work around most missing features, and let us know here if it works for you short-term._ 18 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | # Semantic Commit bot: https://github.com/Ezard/semantic-prs 2 | 3 | # Always validate the PR title, and ignore the commits 4 | titleOnly: true 5 | -------------------------------------------------------------------------------- /.github/workflows/_shared.main.kts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kotlin 2 | @file:DependsOn("io.github.typesafegithub:github-workflows-kt:3.4.0") 3 | 4 | import io.github.typesafegithub.workflows.dsl.expressions.expr 5 | 6 | val disableScheduledJobInForks = 7 | expr { "${github.repository_owner} == 'typesafegithub' || ${github.event_name} != 'schedule'" } 8 | 9 | // The order is deliberate here. First, libraries with no dependencies are published. 10 | // Then, libraries that depend on already published libraries, and so on. Thanks to 11 | // such order, newly released artifacts already have their dependencies in place and 12 | // are ready to be used. 13 | val libraries = listOf( 14 | ":shared-internal", 15 | ":github-workflows-kt", 16 | ":action-binding-generator", 17 | ":action-updates-checker", 18 | ) 19 | -------------------------------------------------------------------------------- /.github/workflows/end-to-end-tests-2nd-workflow.yaml: -------------------------------------------------------------------------------- 1 | # This file was generated using Kotlin DSL (.github/workflows/end-to-end-tests.main.kts). 2 | # If you want to modify the workflow, please change the Kotlin file and regenerate this YAML file. 3 | # Generated with https://github.com/typesafegithub/github-workflows-kt 4 | 5 | name: 'End-to-end tests (2nd workflow)' 6 | on: 7 | push: 8 | branches: 9 | - 'main' 10 | pull_request: {} 11 | jobs: 12 | check_yaml_consistency: 13 | name: 'Check YAML consistency' 14 | runs-on: 'ubuntu-latest' 15 | steps: 16 | - id: 'step-0' 17 | name: 'Check out' 18 | uses: 'actions/checkout@v4' 19 | - id: 'step-1' 20 | name: 'Set up JDK' 21 | uses: 'actions/setup-java@v4' 22 | with: 23 | java-version: '11' 24 | distribution: 'zulu' 25 | - id: 'step-2' 26 | uses: 'gradle/actions/setup-gradle@v4' 27 | - id: 'step-3' 28 | name: 'Publish to Maven local' 29 | run: './gradlew publishToMavenLocal' 30 | - id: 'step-4' 31 | name: 'Execute script' 32 | run: 'rm ''.github/workflows/end-to-end-tests-2nd-workflow.yaml'' && ''.github/workflows/end-to-end-tests.main.kts''' 33 | - id: 'step-5' 34 | name: 'Consistency check' 35 | run: 'git diff --exit-code ''.github/workflows/end-to-end-tests-2nd-workflow.yaml''' 36 | another_job: 37 | runs-on: 'ubuntu-latest' 38 | needs: 39 | - 'check_yaml_consistency' 40 | steps: 41 | - id: 'step-0' 42 | name: 'Check out' 43 | uses: 'actions/checkout@v4' 44 | - id: 'step-1' 45 | name: 'Set up JDK' 46 | uses: 'actions/setup-java@v4' 47 | with: 48 | java-version: '11' 49 | distribution: 'zulu' 50 | - id: 'step-2' 51 | uses: 'gradle/actions/setup-gradle@v4' 52 | - id: 'step-3' 53 | name: 'Publish to Maven local' 54 | run: './gradlew publishToMavenLocal' 55 | - id: 'step-4' 56 | name: 'Step with a Kotlin-based logic, in a different workflow' 57 | env: 58 | GHWKT_GITHUB_CONTEXT_JSON: '${{ toJSON(github) }}' 59 | run: 'GHWKT_RUN_STEP=''end-to-end-tests-2nd-workflow.yaml:another_job:step-4'' ''.github/workflows/end-to-end-tests.main.kts''' 60 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper-validation.main.kts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kotlin 2 | @file:Repository("https://repo.maven.apache.org/maven2/") 3 | @file:DependsOn("io.github.typesafegithub:github-workflows-kt:3.4.0") 4 | 5 | @file:Repository("https://bindings.krzeminski.it") 6 | @file:DependsOn("actions:checkout:v4") 7 | @file:DependsOn("gradle:wrapper-validation-action:v3") 8 | 9 | import io.github.typesafegithub.workflows.actions.actions.Checkout 10 | import io.github.typesafegithub.workflows.actions.gradle.WrapperValidationAction 11 | import io.github.typesafegithub.workflows.domain.RunnerType.UbuntuLatest 12 | import io.github.typesafegithub.workflows.domain.triggers.PullRequest 13 | import io.github.typesafegithub.workflows.domain.triggers.Push 14 | import io.github.typesafegithub.workflows.dsl.workflow 15 | 16 | workflow( 17 | name = "Validate Gradle wrapper", 18 | on = listOf( 19 | Push( 20 | branches = listOf("main"), 21 | paths = listOf("gradle/wrapper/gradle-wrapper.jar"), 22 | ), 23 | PullRequest( 24 | paths = listOf("gradle/wrapper/gradle-wrapper.jar"), 25 | ), 26 | ), 27 | sourceFile = __FILE__, 28 | ) { 29 | job( 30 | id = "validation", 31 | runsOn = UbuntuLatest, 32 | ) { 33 | uses(action = Checkout()) 34 | uses( 35 | name = "Validate wrapper", 36 | action = WrapperValidationAction(), 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper-validation.yaml: -------------------------------------------------------------------------------- 1 | # This file was generated using Kotlin DSL (.github/workflows/gradle-wrapper-validation.main.kts). 2 | # If you want to modify the workflow, please change the Kotlin file and regenerate this YAML file. 3 | # Generated with https://github.com/typesafegithub/github-workflows-kt 4 | 5 | name: 'Validate Gradle wrapper' 6 | on: 7 | push: 8 | branches: 9 | - 'main' 10 | paths: 11 | - 'gradle/wrapper/gradle-wrapper.jar' 12 | pull_request: 13 | paths: 14 | - 'gradle/wrapper/gradle-wrapper.jar' 15 | jobs: 16 | check_yaml_consistency: 17 | name: 'Check YAML consistency' 18 | runs-on: 'ubuntu-latest' 19 | steps: 20 | - id: 'step-0' 21 | name: 'Check out' 22 | uses: 'actions/checkout@v4' 23 | - id: 'step-1' 24 | name: 'Execute script' 25 | run: 'rm ''.github/workflows/gradle-wrapper-validation.yaml'' && ''.github/workflows/gradle-wrapper-validation.main.kts''' 26 | - id: 'step-2' 27 | name: 'Consistency check' 28 | run: 'git diff --exit-code ''.github/workflows/gradle-wrapper-validation.yaml''' 29 | validation: 30 | runs-on: 'ubuntu-latest' 31 | needs: 32 | - 'check_yaml_consistency' 33 | steps: 34 | - id: 'step-0' 35 | uses: 'actions/checkout@v4' 36 | - id: 'step-1' 37 | name: 'Validate wrapper' 38 | uses: 'gradle/wrapper-validation-action@v3' 39 | -------------------------------------------------------------------------------- /.github/workflows/setup-java.main.kts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kotlin 2 | @file:Repository("https://repo.maven.apache.org/maven2/") 3 | @file:DependsOn("io.github.typesafegithub:github-workflows-kt:3.4.0") 4 | 5 | @file:Repository("https://bindings.krzeminski.it") 6 | @file:DependsOn("actions:setup-java:v4") 7 | 8 | import io.github.typesafegithub.workflows.actions.actions.SetupJava 9 | import io.github.typesafegithub.workflows.dsl.JobBuilder 10 | 11 | fun JobBuilder<*>.setupJava() = 12 | uses( 13 | name = "Set up JDK", 14 | action = SetupJava( 15 | javaVersion = "11", 16 | distribution = SetupJava.Distribution.Zulu, 17 | ) 18 | ) 19 | -------------------------------------------------------------------------------- /.github/workflows/setup-python.main.kts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kotlin 2 | @file:Repository("https://repo.maven.apache.org/maven2/") 3 | @file:DependsOn("io.github.typesafegithub:github-workflows-kt:3.4.0") 4 | 5 | @file:Repository("https://bindings.krzeminski.it") 6 | @file:DependsOn("actions:setup-python:v5") 7 | 8 | import io.github.typesafegithub.workflows.actions.actions.SetupPython 9 | import io.github.typesafegithub.workflows.dsl.JobBuilder 10 | 11 | fun JobBuilder<*>.setupPython() = 12 | uses(action = SetupPython(pythonVersion = "3.13")) 13 | -------------------------------------------------------------------------------- /.github/workflows/test-gradle-project-using-bindings-server/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.net.URI 2 | 3 | plugins { 4 | kotlin("jvm") version "2.1.21" 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | maven { 10 | url = URI("http://localhost:8080/") 11 | isAllowInsecureProtocol = true 12 | } 13 | } 14 | 15 | dependencies { 16 | // Regular, top-level action. 17 | implementation("actions:checkout:v4") 18 | 19 | // Nested action. 20 | implementation("gradle:actions__setup-gradle:v3") 21 | 22 | // Using specific version. 23 | implementation("actions:cache:v3.3.3") 24 | 25 | // Always untyped action. 26 | implementation("typesafegithub:always-untyped-action-for-tests:v1") 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/test-gradle-project-using-bindings-server/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typesafegithub/github-workflows-kt/45931670d158f31b3330fe1e6c594be8cadd9a0f/.github/workflows/test-gradle-project-using-bindings-server/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.github/workflows/test-gradle-project-using-bindings-server/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /.github/workflows/test-gradle-project-using-bindings-server/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "test-gradle-project-using-bindings-server" 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/test-gradle-project-using-bindings-server/src/main/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import io.github.typesafegithub.workflows.actions.actions.Cache 2 | import io.github.typesafegithub.workflows.actions.actions.Checkout 3 | import io.github.typesafegithub.workflows.actions.actions.Checkout_Untyped 4 | import io.github.typesafegithub.workflows.actions.gradle.ActionsSetupGradle 5 | import io.github.typesafegithub.workflows.actions.typesafegithub.AlwaysUntypedActionForTests_Untyped 6 | 7 | fun main() { 8 | println(Checkout_Untyped(fetchTags_Untyped = "false")) 9 | println(Checkout(fetchTags = false)) 10 | println(Checkout(fetchTags_Untyped = "false")) 11 | println(AlwaysUntypedActionForTests_Untyped(foobar_Untyped = "baz")) 12 | println(ActionsSetupGradle()) 13 | println(Cache(path = listOf("some-path"), key = "some-key")) 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/test-local-action/action.yml: -------------------------------------------------------------------------------- 1 | name: Test local action 2 | description: I'm a simple local action used in integration tests! 3 | inputs: 4 | name: 5 | description: Name to be used in the greeting. 6 | required: true 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Print greeting 11 | shell: bash 12 | run: echo 'Hello ${{ inputs.name }}!' 13 | -------------------------------------------------------------------------------- /.github/workflows/test-script-consuming-jit-bindings.main.do-not-compile.kts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kotlin 2 | @file:Repository("https://repo.maven.apache.org/maven2/") 3 | @file:DependsOn("io.github.typesafegithub:github-workflows-kt:3.4.0") 4 | @file:DependsOn("io.kotest:kotest-assertions-core:5.9.1") 5 | 6 | @file:Repository("http://localhost:8080") 7 | 8 | // Regular, top-level action. 9 | @file:DependsOn("actions:checkout:v4") 10 | 11 | // Nested action. 12 | @file:DependsOn("gradle:actions__setup-gradle:v3") 13 | 14 | // Using specific version. 15 | @file:DependsOn("actions:cache:v3.3.3") 16 | 17 | // Using version ranges. 18 | @file:DependsOn("gradle:actions__dependency-submission___major:[v3.3.1,v4-alpha)") 19 | @file:DependsOn("gradle:actions__wrapper-validation___minor:[v4.2.1,v4.3-alpha)") 20 | 21 | // Always untyped action. 22 | @file:DependsOn("typesafegithub:always-untyped-action-for-tests:v1") 23 | 24 | import io.github.typesafegithub.workflows.actions.actions.Cache 25 | import io.github.typesafegithub.workflows.actions.actions.Checkout 26 | import io.github.typesafegithub.workflows.actions.actions.Checkout_Untyped 27 | import io.github.typesafegithub.workflows.actions.gradle.ActionsSetupGradle 28 | import io.github.typesafegithub.workflows.actions.gradle.ActionsDependencySubmission_Untyped 29 | import io.github.typesafegithub.workflows.actions.gradle.ActionsWrapperValidation 30 | import io.github.typesafegithub.workflows.actions.typesafegithub.AlwaysUntypedActionForTests_Untyped 31 | import io.kotest.matchers.shouldBe 32 | 33 | println(Checkout_Untyped(fetchTags_Untyped = "false")) 34 | println(Checkout(fetchTags = false)) 35 | println(Checkout(fetchTags_Untyped = "false")) 36 | println(AlwaysUntypedActionForTests_Untyped(foobar_Untyped = "baz")) 37 | println(ActionsSetupGradle()) 38 | println(Cache(path = listOf("some-path"), key = "some-key")) 39 | 40 | ActionsDependencySubmission_Untyped().actionVersion shouldBe "v3" 41 | ActionsWrapperValidation().actionVersion shouldBe "v4.2" 42 | 43 | // Ensure that 'copy(...)' method is exposed. 44 | Checkout(fetchTags = false).copy(fetchTags = true) 45 | -------------------------------------------------------------------------------- /.github/workflows/test-served-bindings-depend-on-library.main.do-not-compile.kts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env kotlin 2 | @file:Repository("https://repo.maven.apache.org/maven2/") 3 | @file:Repository("http://localhost:8080") 4 | @file:DependsOn("actions:checkout:v4") 5 | 6 | import io.github.typesafegithub.workflows.actions.actions.Checkout 7 | 8 | Checkout() 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | 3 | .idea 4 | !.idea/externalDependencies.xml # Required IntelliJ plugins. 5 | !.idea/icon.svg 6 | .DS_Store 7 | .kotlin 8 | 9 | build 10 | 11 | # Created in unit tests - easier and more readable to ignore it rather than adjust the test. 12 | /.github/workflows/.yaml 13 | .github/workflows/some_workflow.yaml 14 | 15 | # Action bindings are generated by CI each time, and locally we should regenerate it as often as possible. 16 | /.github/workflows/generated 17 | 18 | /jit-binding-server/logs/ 19 | /logs/ 20 | -------------------------------------------------------------------------------- /.idea/externalDependencies.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | Thanks for finding a moment for your contribution! We really appreciate it! 4 | The below rules are here to speed things up and streamline work on both sides - us as library maintainers and you as the contributor. 5 | 6 | ## Reporting a bug 7 | 8 | First check if it's not already reported. Use the "bug" tag for filtering. 9 | If it's not, create a new one using the template. If it already exists, give it a thumb-up and optionally share any helpful details in the comments. 10 | 11 | ## Requesting a feature 12 | 13 | There's also a designated template for it, and the rule of checking if the request is already tracked also applies. 14 | 15 | ## Contributing a feature 16 | 17 | Each feature contribution must have an accompanying feature request issue, best if we discuss the approach in the issue first to avoid misunderstandings and wasted time. If the feature is simple, go ahead with creating the issue and the PR in parallel. 18 | 19 | If you're creating a new feature branch in this repository (doesn't count if it's your fork), it has to start with issue number, e.g. `123-new-steps-API`, and the issue has to be open. Otherwise, the branch is assumed to be safe to delete. 20 | -------------------------------------------------------------------------------- /MAINTENANCE.md: -------------------------------------------------------------------------------- 1 | This file describes various maintenance tasks, relevant for project maintainers only. 2 | 3 | # Release a new version 4 | 5 | It currently happens whenever necessary, there's no agreed cadence. Whenever we see there's an important bug fix or a feature to roll out, or an important dependency update, we release. 6 | 7 | 1. Remove `-SNAPSHOT` for version starting from [build.gradle.kts](https://github.com/typesafegithub/github-workflows-kt/blob/main/build.gradle.kts). By building the whole project with `./gradlew build`, you will learn what other places need to be adjusted. There's one place that needs extra care: in PomBuilding.kt, there's `LATEST_RELASED_LIBRARY_VERSION` - set it to the version you're going to deploy in a minute. Once done, create a commit using this pattern for commit message: `chore: prepare for releasing version `. 8 | 1. Once CI is green for the newly merged commits, create and push an annotated tag: 9 | ``` 10 | COMMIT_TITLE=`git log -1 --pretty=%B` 11 | VERSION=${COMMIT_TITLE#"chore: prepare for releasing version "} 12 | git tag -a "v$VERSION" -m "chore: release version $VERSION" && git push origin "v$VERSION" 13 | ``` 14 | 1. On `main` branch, change version to prepare for the next development cycle, e.g. if it was `1.2.3-SNAPSHOT` before and we released it as `1.2.3`, change the version to `1.2.3-SNAPSHOT`. 15 | 1. Ensure that the release job has succeeded. 16 | -------------------------------------------------------------------------------- /action-binding-generator/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jmailen.gradle.kotlinter.tasks.ConfigurableKtLintTask 2 | 3 | plugins { 4 | buildsrc.convention.`kotlin-jvm` 5 | buildsrc.convention.publishing 6 | kotlin("plugin.serialization") 7 | 8 | id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.17.0" 9 | } 10 | 11 | group = rootProject.group 12 | version = rootProject.version 13 | 14 | dependencies { 15 | implementation("com.squareup:kotlinpoet:2.2.0") 16 | implementation("com.charleskorn.kaml:kaml:0.80.1") 17 | implementation("io.github.oshai:kotlin-logging:7.0.7") 18 | implementation(projects.sharedInternal) 19 | 20 | testImplementation(projects.githubWorkflowsKt) 21 | } 22 | 23 | kotlin { 24 | explicitApi() 25 | } 26 | 27 | fun ConfigurableKtLintTask.kotlinterConfig() { 28 | exclude("**/bindingsfromunittests/**") 29 | } 30 | 31 | tasks.lintKotlinTest { 32 | kotlinterConfig() 33 | } 34 | 35 | tasks.formatKotlinTest { 36 | kotlinterConfig() 37 | } 38 | -------------------------------------------------------------------------------- /action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.domain 2 | 3 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL 4 | 5 | public data class ActionCoords( 6 | val owner: String, 7 | val name: String, 8 | val version: String, 9 | /** 10 | * The version part that is significant when generating the YAML output, 11 | * i.e. whether to write the full version, only the major version or major and minor version. 12 | * This is used to enable usage of Maven ranges without needing to specify a custom version 13 | * each time instantiating an action. 14 | * The value of this property is part of the Maven coordinates as a suffix for the [name] property. 15 | */ 16 | val significantVersion: SignificantVersion = FULL, 17 | val path: String? = null, 18 | ) 19 | 20 | /** 21 | * A top-level action is an action with its `action.y(a)ml` file in the repository root, as opposed to actions stored 22 | * in subdirectories. 23 | */ 24 | public val ActionCoords.isTopLevel: Boolean get() = path == null 25 | 26 | public val ActionCoords.prettyPrint: String get() = "$prettyPrintWithoutVersion@$version" 27 | 28 | public val ActionCoords.prettyPrintWithoutVersion: String get() = "$owner/$fullName${ 29 | significantVersion.takeUnless { it == FULL }?.let { " with $it version" } ?: "" 30 | }" 31 | 32 | /** 33 | * For most actions, it's empty. 34 | * For actions that aren't executed from the root of the repo, it returns the path relative to the repo root where the 35 | * action lives, starting with a slash. 36 | */ 37 | public val ActionCoords.subName: String get() = path?.let { "/$path" } ?: "" 38 | 39 | /** 40 | * For most actions, it's equal to [ActionCoords.name]. 41 | * For actions that aren't executed from the root of the repo, it returns the path starting with the repo root where the 42 | * action lives. 43 | */ 44 | public val ActionCoords.fullName: String get() = "$name$subName" 45 | -------------------------------------------------------------------------------- /action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/MetadataRevision.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.domain 2 | 3 | public sealed interface MetadataRevision 4 | 5 | public data object NewestForVersion : MetadataRevision 6 | 7 | public data class CommitHash( 8 | val value: String, 9 | ) : MetadataRevision 10 | -------------------------------------------------------------------------------- /action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/SignificantVersion.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.domain 2 | 3 | /** 4 | * The version part that is significant when generating the YAML output. 5 | * This is used to enable usage of Maven ranges without needing to specify a custom version 6 | * each time instantiating an action. 7 | */ 8 | public enum class SignificantVersion { 9 | /** 10 | * Only write the major version to the generated YAML. 11 | */ 12 | MAJOR, 13 | 14 | /** 15 | * Only write the major and minor version to the generated YAML. 16 | */ 17 | MINOR, 18 | 19 | /** 20 | * Write the full version to the generated YAML. 21 | */ 22 | FULL, 23 | ; 24 | 25 | override fun toString(): String = super.toString().lowercase() 26 | } 27 | -------------------------------------------------------------------------------- /action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/TypingActualSource.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.domain 2 | 3 | public enum class TypingActualSource { 4 | ACTION, 5 | TYPING_CATALOG, 6 | } 7 | -------------------------------------------------------------------------------- /action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/generation/ClassNaming.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.generation 2 | 3 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords 4 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.fullName 5 | import io.github.typesafegithub.workflows.actionbindinggenerator.utils.toPascalCase 6 | 7 | internal fun ActionCoords.buildActionClassName(): String = this.fullName.toPascalCase() 8 | -------------------------------------------------------------------------------- /action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/metadata/InputNullability.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.metadata 2 | 3 | /** 4 | * [Input.required] is in theory a required field in action's metadata, but in practice a lot of actions don't specify 5 | * it. It's thus a challenge to infer nullability of inputs in the Kotlin bindings. This function tackles this task. 6 | */ 7 | internal fun Input.shouldBeRequiredInBinding() = default == null && required == true 8 | -------------------------------------------------------------------------------- /action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/metadata/MetadataReading.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.metadata 2 | 3 | import com.charleskorn.kaml.Yaml 4 | import io.github.oshai.kotlinlogging.KotlinLogging.logger 5 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords 6 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.CommitHash 7 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.MetadataRevision 8 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.NewestForVersion 9 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.subName 10 | import kotlinx.serialization.Serializable 11 | import kotlinx.serialization.decodeFromString 12 | import java.io.IOException 13 | import java.net.URI 14 | 15 | private val logger = logger { } 16 | 17 | /** 18 | * [Metadata syntax for GitHub Actions](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions). 19 | */ 20 | 21 | @Serializable 22 | public data class Metadata( 23 | val name: String, 24 | val description: String, 25 | val inputs: Map = emptyMap(), 26 | val outputs: Map = emptyMap(), 27 | ) 28 | 29 | @Serializable 30 | public data class Input( 31 | val description: String = "", 32 | val default: String? = null, 33 | val required: Boolean? = null, 34 | val deprecationMessage: String? = null, 35 | ) 36 | 37 | @Serializable 38 | public data class Output( 39 | val description: String = "", 40 | ) 41 | 42 | private fun ActionCoords.actionYmlUrl(gitRef: String) = "https://raw.githubusercontent.com/$owner/$name/$gitRef$subName/action.yml" 43 | 44 | private fun ActionCoords.actionYamlUrl(gitRef: String) = "https://raw.githubusercontent.com/$owner/$name/$gitRef$subName/action.yaml" 45 | 46 | public fun ActionCoords.fetchMetadata( 47 | metadataRevision: MetadataRevision, 48 | fetchUri: (URI) -> String = ::fetchUri, 49 | ): Metadata? { 50 | val gitRef = 51 | when (metadataRevision) { 52 | is CommitHash -> metadataRevision.value 53 | NewestForVersion -> this.version 54 | } 55 | val list = listOf(actionYmlUrl(gitRef), actionYamlUrl(gitRef)) 56 | 57 | return list 58 | .firstNotNullOfOrNull { url -> 59 | try { 60 | logger.info { " ... from $url" } 61 | fetchUri(URI(url)) 62 | } catch (e: IOException) { 63 | null 64 | } 65 | }?.let { yaml.decodeFromString(it) } 66 | } 67 | 68 | internal fun fetchUri(uri: URI): String = uri.toURL().readText() 69 | 70 | private val yaml = 71 | Yaml( 72 | configuration = 73 | Yaml.default.configuration.copy( 74 | strictMode = false, 75 | ), 76 | ) 77 | -------------------------------------------------------------------------------- /action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/typing/ActionTypes.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.typing 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | internal data class ActionTypes( 8 | val inputs: Map? = null, 9 | val outputs: Map? = null, 10 | ) 11 | 12 | @Serializable 13 | internal data class ActionType( 14 | val type: ActionTypeEnum, 15 | val name: String? = null, 16 | @SerialName("named-values") 17 | val namedValues: Map = emptyMap(), 18 | val separator: String = "", 19 | @SerialName("allowed-values") 20 | val allowedValues: List = emptyList(), 21 | @SerialName("list-item") 22 | val listItem: ActionType? = null, 23 | ) { 24 | init { 25 | validateType() 26 | } 27 | } 28 | 29 | @Serializable 30 | internal enum class ActionTypeEnum { 31 | @SerialName("string") 32 | String, 33 | 34 | @SerialName("boolean") 35 | Boolean, 36 | 37 | @SerialName("integer") 38 | Integer, 39 | 40 | @SerialName("float") 41 | Float, 42 | 43 | @SerialName("list") 44 | List, 45 | 46 | @SerialName("enum") 47 | Enum, 48 | } 49 | 50 | internal fun ActionType.validateType() { 51 | if (type == ActionTypeEnum.List) { 52 | check(separator.isNotEmpty() && listItem != null) { 53 | "Invalid type $this: needs a separator and a listItem" 54 | } 55 | } else if (type == ActionTypeEnum.Enum) { 56 | check(allowedValues.isNotEmpty()) { 57 | "Invalid type $this: needs allowedValues" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/typing/Typing.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.typing 2 | 3 | public sealed interface Typing 4 | 5 | internal object BooleanTyping : Typing 6 | 7 | internal data class EnumTyping( 8 | /** 9 | * Custom type name when used in Kotlin code. 10 | * Null means that the type name should be inferred from the input name. 11 | */ 12 | val typeName: String? = null, 13 | /** 14 | * Items as required by the action, e.g. foo-bar-1 15 | */ 16 | val items: List, 17 | /** 18 | * Items in a more readable form when used in Kotlin code and following its conventions, e.g. FooBar1. 19 | * Null means that these names should be inferred from the items. 20 | */ 21 | val itemsNames: List? = null, 22 | ) : Typing 23 | 24 | internal object FloatTyping : Typing 25 | 26 | internal object IntegerTyping : Typing 27 | 28 | internal data class IntegerWithSpecialValueTyping( 29 | /** 30 | * Custom type name when used in Kotlin code. 31 | * Null means that the type name should be inferred from the input name. 32 | */ 33 | val typeName: String? = null, 34 | val specialValues: Map, 35 | ) : Typing 36 | 37 | internal data class ListOfTypings( 38 | val delimiter: String, 39 | val typing: Typing = StringTyping, 40 | ) : Typing 41 | 42 | internal object StringTyping : Typing 43 | -------------------------------------------------------------------------------- /action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/utils/TextUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.utils 2 | 3 | import java.util.Locale 4 | 5 | private val normalizationSplitRegex = """[^\p{javaJavaIdentifierStart}\p{javaJavaIdentifierPart}&&[^_]]++""".toRegex() 6 | 7 | internal fun String.toPascalCase(): String { 8 | val hasOnlyUppercases = none { it in 'a'..'z' } 9 | val normalizedString = if (hasOnlyUppercases) lowercase() else this 10 | return normalizedString 11 | .replace("+", "-plus-") 12 | .split(normalizationSplitRegex) 13 | .joinToString("") { 14 | it.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } 15 | } 16 | } 17 | 18 | internal fun String.toCamelCase(): String = toPascalCase().replaceFirstChar { it.lowercase() } 19 | 20 | internal fun String.toKotlinPackageName(): String = 21 | replace("-", "") 22 | .lowercase() 23 | 24 | internal fun String.removeTrailingWhitespacesForEachLine() = lines().joinToString(separator = "\n") { it.trimEnd() } 25 | -------------------------------------------------------------------------------- /action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/Utils.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator 2 | 3 | import io.github.typesafegithub.workflows.actionbindinggenerator.generation.ActionBinding 4 | import io.kotest.matchers.Matcher.Companion.failure 5 | import io.kotest.matchers.shouldBe 6 | import java.nio.file.Paths 7 | 8 | fun List.shouldContainAndMatchFile(path: String) { 9 | val binding = 10 | this 11 | .firstOrNull { it.filePath.endsWith(path) } 12 | ?: error("Binding with path ending with $path not found!") 13 | val file = 14 | Paths 15 | .get("src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/bindingsfromunittests/$path") 16 | .toFile() 17 | val expectedContent = 18 | when { 19 | file.canRead() -> file.readText().removeWindowsNewLines() 20 | else -> "" 21 | } 22 | val actualContent = binding.kotlinCode.removeWindowsNewLines() 23 | 24 | binding.filePath shouldBe "io/github/typesafegithub/workflows/actions/johnsmith/$path" 25 | 26 | if (System.getenv("GITHUB_ACTIONS") == "true") { 27 | actualContent shouldBe expectedContent 28 | } else if (actualContent != expectedContent) { 29 | file.writeText(actualContent) 30 | actualContent shouldBe 31 | failure( 32 | "The binding's Kotlin code in ${file.name} doesn't match the expected one.\n" + 33 | "The file has been updated to match what's expected.", 34 | ) 35 | } 36 | } 37 | 38 | private fun String.removeWindowsNewLines(): String = replace("\r\n", "\n") 39 | -------------------------------------------------------------------------------- /action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/bindingsfromunittests/ActionWithNoInputs.kt: -------------------------------------------------------------------------------- 1 | // This file was generated using action-binding-generator. Don't change it by hand, otherwise your 2 | // changes will be overwritten with the next binding code regeneration. 3 | // See https://github.com/typesafegithub/github-workflows-kt for more info. 4 | @file:Suppress( 5 | "DataClassPrivateConstructor", 6 | "UNUSED_PARAMETER", 7 | ) 8 | 9 | package io.github.typesafegithub.workflows.actions.johnsmith 10 | 11 | import io.github.typesafegithub.workflows.domain.actions.Action 12 | import io.github.typesafegithub.workflows.domain.actions.RegularAction 13 | import java.util.LinkedHashMap 14 | import kotlin.ExposedCopyVisibility 15 | import kotlin.String 16 | import kotlin.Suppress 17 | import kotlin.Unit 18 | import kotlin.collections.Map 19 | 20 | /** 21 | * Action: Action With No Inputs 22 | * 23 | * Description 24 | * 25 | * [Action on GitHub](https://github.com/john-smith/action-with-no-inputs) 26 | * 27 | * @param _customInputs Type-unsafe map where you can put any inputs that are not yet supported by the binding 28 | * @param _customVersion Allows overriding action's version, for example to use a specific minor version, or a newer version that the binding doesn't yet know about 29 | */ 30 | @ExposedCopyVisibility 31 | public data class ActionWithNoInputs private constructor( 32 | /** 33 | * Type-unsafe map where you can put any inputs that are not yet supported by the binding 34 | */ 35 | public val _customInputs: Map = mapOf(), 36 | /** 37 | * Allows overriding action's version, for example to use a specific minor version, or a newer version that the binding doesn't yet know about 38 | */ 39 | public val _customVersion: String? = null, 40 | ) : RegularAction("john-smith", "action-with-no-inputs", _customVersion ?: "v3") { 41 | public constructor( 42 | vararg pleaseUseNamedArguments: Unit, 43 | _customInputs: Map = mapOf(), 44 | _customVersion: String? = null, 45 | ) : this(_customInputs = _customInputs, _customVersion = _customVersion) 46 | 47 | @Suppress("SpreadOperator") 48 | override fun toYamlArguments(): LinkedHashMap = LinkedHashMap(_customInputs) 49 | 50 | override fun buildOutputObject(stepId: String): Action.Outputs = Outputs(stepId) 51 | } 52 | -------------------------------------------------------------------------------- /action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/bindingsfromunittests/ActionWithNoInputsWithMajorVersion.kt: -------------------------------------------------------------------------------- 1 | // This file was generated using action-binding-generator. Don't change it by hand, otherwise your 2 | // changes will be overwritten with the next binding code regeneration. 3 | // See https://github.com/typesafegithub/github-workflows-kt for more info. 4 | @file:Suppress( 5 | "DataClassPrivateConstructor", 6 | "UNUSED_PARAMETER", 7 | ) 8 | 9 | package io.github.typesafegithub.workflows.actions.johnsmith 10 | 11 | import io.github.typesafegithub.workflows.domain.actions.Action 12 | import io.github.typesafegithub.workflows.domain.actions.RegularAction 13 | import java.util.LinkedHashMap 14 | import kotlin.ExposedCopyVisibility 15 | import kotlin.String 16 | import kotlin.Suppress 17 | import kotlin.Unit 18 | import kotlin.collections.Map 19 | 20 | /** 21 | * Action: Action With No Inputs 22 | * 23 | * Description 24 | * 25 | * [Action on GitHub](https://github.com/john-smith/action-with-no-inputs-with-major-version) 26 | * 27 | * @param _customInputs Type-unsafe map where you can put any inputs that are not yet supported by the binding 28 | * @param _customVersion Allows overriding action's version, for example to use a specific minor version, or a newer version that the binding doesn't yet know about 29 | */ 30 | @ExposedCopyVisibility 31 | public data class ActionWithNoInputsWithMajorVersion private constructor( 32 | /** 33 | * Type-unsafe map where you can put any inputs that are not yet supported by the binding 34 | */ 35 | public val _customInputs: Map = mapOf(), 36 | /** 37 | * Allows overriding action's version, for example to use a specific minor version, or a newer version that the binding doesn't yet know about 38 | */ 39 | public val _customVersion: String? = null, 40 | ) : RegularAction("john-smith", "action-with-no-inputs-with-major-version", _customVersion ?: "v3") { 41 | public constructor( 42 | vararg pleaseUseNamedArguments: Unit, 43 | _customInputs: Map = mapOf(), 44 | _customVersion: String? = null, 45 | ) : this(_customInputs = _customInputs, _customVersion = _customVersion) 46 | 47 | @Suppress("SpreadOperator") 48 | override fun toYamlArguments(): LinkedHashMap = LinkedHashMap(_customInputs) 49 | 50 | override fun buildOutputObject(stepId: String): Action.Outputs = Outputs(stepId) 51 | } 52 | -------------------------------------------------------------------------------- /action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/bindingsfromunittests/ActionWithNoInputsWithMinorVersion.kt: -------------------------------------------------------------------------------- 1 | // This file was generated using action-binding-generator. Don't change it by hand, otherwise your 2 | // changes will be overwritten with the next binding code regeneration. 3 | // See https://github.com/typesafegithub/github-workflows-kt for more info. 4 | @file:Suppress( 5 | "DataClassPrivateConstructor", 6 | "UNUSED_PARAMETER", 7 | ) 8 | 9 | package io.github.typesafegithub.workflows.actions.johnsmith 10 | 11 | import io.github.typesafegithub.workflows.domain.actions.Action 12 | import io.github.typesafegithub.workflows.domain.actions.RegularAction 13 | import java.util.LinkedHashMap 14 | import kotlin.ExposedCopyVisibility 15 | import kotlin.String 16 | import kotlin.Suppress 17 | import kotlin.Unit 18 | import kotlin.collections.Map 19 | 20 | /** 21 | * Action: Action With No Inputs 22 | * 23 | * Description 24 | * 25 | * [Action on GitHub](https://github.com/john-smith/action-with-no-inputs-with-minor-version) 26 | * 27 | * @param _customInputs Type-unsafe map where you can put any inputs that are not yet supported by the binding 28 | * @param _customVersion Allows overriding action's version, for example to use a specific minor version, or a newer version that the binding doesn't yet know about 29 | */ 30 | @ExposedCopyVisibility 31 | public data class ActionWithNoInputsWithMinorVersion private constructor( 32 | /** 33 | * Type-unsafe map where you can put any inputs that are not yet supported by the binding 34 | */ 35 | public val _customInputs: Map = mapOf(), 36 | /** 37 | * Allows overriding action's version, for example to use a specific minor version, or a newer version that the binding doesn't yet know about 38 | */ 39 | public val _customVersion: String? = null, 40 | ) : RegularAction("john-smith", "action-with-no-inputs-with-minor-version", _customVersion ?: "v3.1") { 41 | public constructor( 42 | vararg pleaseUseNamedArguments: Unit, 43 | _customInputs: Map = mapOf(), 44 | _customVersion: String? = null, 45 | ) : this(_customInputs = _customInputs, _customVersion = _customVersion) 46 | 47 | @Suppress("SpreadOperator") 48 | override fun toYamlArguments(): LinkedHashMap = LinkedHashMap(_customInputs) 49 | 50 | override fun buildOutputObject(stepId: String): Action.Outputs = Outputs(stepId) 51 | } 52 | -------------------------------------------------------------------------------- /action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/bindingsfromunittests/ActionWithOutputsTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.bindingsfromunittests 2 | 3 | import io.github.typesafegithub.workflows.actions.johnsmith.ActionWithOutputs 4 | import io.kotest.core.spec.style.DescribeSpec 5 | import io.kotest.matchers.shouldBe 6 | 7 | class ActionWithOutputsTest : DescribeSpec({ 8 | it("fields have correct output placeholders") { 9 | // given 10 | val outputs = ActionWithOutputs(fooBar = "1").buildOutputObject("someStepId") 11 | 12 | // when & then 13 | outputs.bazGoo shouldBe "steps.someStepId.outputs.baz-goo" 14 | outputs.looWoz shouldBe "steps.someStepId.outputs.loo-woz" 15 | outputs["custom-output"] shouldBe "steps.someStepId.outputs.custom-output" 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/bindingsfromunittests/ActionWithSomeOptionalInputsTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.bindingsfromunittests 2 | 3 | import io.github.typesafegithub.workflows.actions.johnsmith.ActionWithSomeOptionalInputs 4 | import io.kotest.core.spec.style.DescribeSpec 5 | import io.kotest.matchers.shouldBe 6 | 7 | class ActionWithSomeOptionalInputsTest : DescribeSpec({ 8 | it("renders with defaults") { 9 | // given 10 | val action = ActionWithSomeOptionalInputs( 11 | bazGoo = "def456", 12 | `package` = "qwe789" 13 | ) 14 | 15 | // when 16 | val yaml = action.toYamlArguments() 17 | 18 | // then 19 | yaml shouldBe linkedMapOf( 20 | "baz-goo" to "def456", 21 | "package" to "qwe789", 22 | ) 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/bindingsfromunittests/ActionWithSubAction.kt: -------------------------------------------------------------------------------- 1 | // This file was generated using action-binding-generator. Don't change it by hand, otherwise your 2 | // changes will be overwritten with the next binding code regeneration. 3 | // See https://github.com/typesafegithub/github-workflows-kt for more info. 4 | @file:Suppress( 5 | "DataClassPrivateConstructor", 6 | "UNUSED_PARAMETER", 7 | ) 8 | 9 | package io.github.typesafegithub.workflows.actions.johnsmith 10 | 11 | import io.github.typesafegithub.workflows.domain.actions.Action 12 | import io.github.typesafegithub.workflows.domain.actions.RegularAction 13 | import java.util.LinkedHashMap 14 | import kotlin.ExposedCopyVisibility 15 | import kotlin.String 16 | import kotlin.Suppress 17 | import kotlin.Unit 18 | import kotlin.collections.Map 19 | 20 | /** 21 | * Action: Action With No Inputs 22 | * 23 | * Description 24 | * 25 | * [Action on GitHub](https://github.com/john-smith/action-with/tree/v3/sub/action) 26 | * 27 | * @param _customInputs Type-unsafe map where you can put any inputs that are not yet supported by the binding 28 | * @param _customVersion Allows overriding action's version, for example to use a specific minor version, or a newer version that the binding doesn't yet know about 29 | */ 30 | @ExposedCopyVisibility 31 | public data class ActionWithSubAction private constructor( 32 | /** 33 | * Type-unsafe map where you can put any inputs that are not yet supported by the binding 34 | */ 35 | public val _customInputs: Map = mapOf(), 36 | /** 37 | * Allows overriding action's version, for example to use a specific minor version, or a newer version that the binding doesn't yet know about 38 | */ 39 | public val _customVersion: String? = null, 40 | ) : RegularAction("john-smith", "action-with/sub/action", _customVersion ?: "v3") { 41 | public constructor( 42 | vararg pleaseUseNamedArguments: Unit, 43 | _customInputs: Map = mapOf(), 44 | _customVersion: String? = null, 45 | ) : this(_customInputs = _customInputs, _customVersion = _customVersion) 46 | 47 | @Suppress("SpreadOperator") 48 | override fun toYamlArguments(): LinkedHashMap = LinkedHashMap(_customInputs) 49 | 50 | override fun buildOutputObject(stepId: String): Action.Outputs = Outputs(stepId) 51 | } 52 | -------------------------------------------------------------------------------- /action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/bindingsfromunittests/SimpleActionWithRequiredStringInputsTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.bindingsfromunittests 2 | 3 | import io.github.typesafegithub.workflows.actions.johnsmith.SimpleActionWithRequiredStringInputs 4 | import io.kotest.core.spec.style.DescribeSpec 5 | import io.kotest.matchers.shouldBe 6 | 7 | class SimpleActionWithRequiredStringInputsTest : DescribeSpec({ 8 | it("renders with defaults") { 9 | // given 10 | val action = SimpleActionWithRequiredStringInputs( 11 | fooBar = "abc123", 12 | bazGoo = "def456", 13 | ) 14 | 15 | // when 16 | val yaml = action.toYamlArguments() 17 | 18 | // then 19 | yaml shouldBe linkedMapOf( 20 | "foo-bar" to "abc123", 21 | "baz-goo" to "def456", 22 | ) 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/generation/ClassNamingTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.generation 2 | 3 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords 4 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL 5 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.MAJOR 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.matchers.shouldBe 8 | 9 | class ClassNamingTest : 10 | FunSpec({ 11 | context("buildActionClassName") { 12 | listOf( 13 | ActionCoords("irrelevant", "some-action-name", "v2") to "SomeActionName", 14 | ActionCoords("irrelevant", "some-action-name", "v2", FULL, "subaction") to "SomeActionNameSubaction", 15 | ActionCoords("irrelevant", "some-action-name", "v2", FULL, "foo/bar/baz") to "SomeActionNameFooBarBaz", 16 | ActionCoords("irrelevant", "some-action-name", "v2", MAJOR, "foo/bar/baz") to "SomeActionNameFooBarBaz", 17 | ).forEach { (input, output) -> 18 | test("should get '$input' and produce '$output'") { 19 | input.buildActionClassName() shouldBe output 20 | } 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/metadata/InputNullabilityTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.metadata 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.data.row 5 | import io.kotest.matchers.shouldBe 6 | 7 | class InputNullabilityTest : 8 | FunSpec({ 9 | context("shouldBeNonNullInBinding") { 10 | listOf( 11 | // No info about default value or input being required, so let's assume more freedom here - nullable. 12 | row(null, null, false), 13 | // Default given so let's assume it's nullable. 14 | row("some default", null, false), 15 | // Default not given and not required: assuming some default, empty value is provided by GH Actions. 16 | row(null, false, false), 17 | // Default given and not required: it's definitely nullable. 18 | row("some default", false, false), 19 | // Required = true, a no-brainer it should be non-null. 20 | row(null, true, true), 21 | // A required input with default: it should never happen, but let's try allowing null here. 22 | row("some default", true, false), 23 | ).forEach { (default, required, result) -> 24 | test("test for default = $default, required = $required - should be non-null: $result") { 25 | Input( 26 | description = "Some input", 27 | default = default, 28 | required = required, 29 | ).shouldBeRequiredInBinding() shouldBe result 30 | } 31 | } 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/utils/TextUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actionbindinggenerator.utils 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class TextUtilsTest : 7 | FunSpec({ 8 | context("toPascalCase") { 9 | listOf( 10 | "some-name" to "SomeName", 11 | "some_name" to "SomeName", 12 | "SOME_PROPERTY" to "SomeProperty", 13 | "some+name" to "SomePlusName", 14 | "some name with spaces" to "SomeNameWithSpaces", 15 | "some.name.with.dots" to "SomeNameWithDots", 16 | "some-action/some-subdirectory" to "SomeActionSomeSubdirectory", 17 | ).forEach { (input, output) -> 18 | test("should convert '$input' to '$output'") { 19 | input.toPascalCase() shouldBe output 20 | } 21 | } 22 | } 23 | 24 | context("toCamelCase") { 25 | listOf( 26 | "some-name" to "someName", 27 | "some_name" to "someName", 28 | "some+name" to "somePlusName", 29 | "SOME_NAME" to "someName", 30 | ).forEach { (input, output) -> 31 | test("should convert '$input' to '$output'") { 32 | input.toCamelCase() shouldBe output 33 | } 34 | } 35 | } 36 | 37 | context("toKotlinPackageName") { 38 | listOf( 39 | "some-name" to "somename", 40 | "SomeName" to "somename", 41 | ).forEach { (input, output) -> 42 | test("should convert '$input' to '$output'") { 43 | input.toKotlinPackageName() shouldBe output 44 | } 45 | } 46 | } 47 | }) 48 | -------------------------------------------------------------------------------- /action-binding-generator/src/test/resources/types/action-types.yml: -------------------------------------------------------------------------------- 1 | # See https://github.com/typesafegithub/github-actions-typing 2 | inputs: 3 | script: 4 | type: string 5 | github-token: 6 | type: string 7 | debug: 8 | type: boolean 9 | user-agent: 10 | type: string 11 | previews: 12 | type: list 13 | separator: ',' 14 | list-item: 15 | type: string 16 | result-encoding: 17 | type: enum 18 | allowed-values: 19 | - string 20 | - json 21 | # Please check those outputs's description and set a proper type. 'string' is just set by default 22 | outputs: 23 | result: 24 | type: string 25 | -------------------------------------------------------------------------------- /action-binding-generator/src/test/resources/types/action.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Script 2 | author: GitHub 3 | description: Run simple scripts using the GitHub client 4 | branding: 5 | color: blue 6 | icon: code 7 | inputs: 8 | script: 9 | description: The script to run 10 | required: true 11 | github-token: 12 | description: The GitHub token used to create an authenticated client 13 | default: ${{ github.token }} 14 | required: false 15 | debug: 16 | description: Whether to tell the GitHub client to log details of its requests 17 | default: "false" 18 | required: false 19 | user-agent: 20 | description: An optional user-agent string 21 | default: actions/github-script 22 | required: false 23 | previews: 24 | description: A comma-separated list of API previews to accept 25 | required: false 26 | result-encoding: 27 | description: Either "string" or "json" (default "json")—how the result will be encoded 28 | default: json 29 | required: false 30 | outputs: 31 | result: 32 | description: The return value of the script, stringified with `JSON.stringify` 33 | runs: 34 | using: node16 35 | main: dist/index.js 36 | -------------------------------------------------------------------------------- /action-updates-checker/api/action-updates-checker.api: -------------------------------------------------------------------------------- 1 | public final class io/github/typesafegithub/workflows/updates/ReportingKt { 2 | public static final fun reportAvailableUpdates (Lio/github/typesafegithub/workflows/domain/Workflow;ZLjava/lang/String;)V 3 | public static synthetic fun reportAvailableUpdates$default (Lio/github/typesafegithub/workflows/domain/Workflow;ZLjava/lang/String;ILjava/lang/Object;)V 4 | } 5 | 6 | -------------------------------------------------------------------------------- /action-updates-checker/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | buildsrc.convention.`kotlin-jvm` 3 | buildsrc.convention.publishing 4 | 5 | id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.17.0" 6 | } 7 | 8 | group = rootProject.group 9 | version = rootProject.version 10 | 11 | dependencies { 12 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") 13 | 14 | implementation(projects.githubWorkflowsKt) 15 | implementation(projects.sharedInternal) 16 | } 17 | 18 | kotlin { 19 | explicitApi() 20 | } 21 | -------------------------------------------------------------------------------- /action-updates-checker/src/main/kotlin/io/github/typesafegithub/workflows/updates/model/RegularActionVersions.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.updates.model 2 | 3 | import io.github.typesafegithub.workflows.domain.ActionStep 4 | import io.github.typesafegithub.workflows.domain.actions.RegularAction 5 | import io.github.typesafegithub.workflows.shared.internal.model.Version 6 | 7 | internal data class RegularActionVersions( 8 | val action: RegularAction<*>, 9 | val steps: List>, 10 | val newerVersions: List, 11 | val availableVersions: List, 12 | ) 13 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | base 3 | 4 | // Publishing. 5 | id("io.github.gradle-nexus.publish-plugin") version "2.0.0" // Needs to be applied to the root project. 6 | } 7 | 8 | group = "io.github.typesafegithub" 9 | version = "3.4.1-SNAPSHOT" 10 | 11 | nexusPublishing { 12 | repositories { 13 | sonatype { 14 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 15 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 16 | } 17 | } 18 | packageGroup.set("io.github.typesafegithub") 19 | } 20 | 21 | val setIsSnapshotFlagInGithubOutput by tasks.registering { 22 | // This property of a project needs to be resolved before the 'doLast' block because otherwise, Gradle 23 | // configuration cache cannot be used. 24 | val version = version 25 | 26 | doLast { 27 | val filePath = System.getenv("GITHUB_OUTPUT") ?: error("Expected GITHUB_OUTPUT variable to be set!") 28 | val isSnapshot = version.toString().endsWith("-SNAPSHOT") 29 | File(filePath).appendText("is-snapshot=$isSnapshot\n") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | kotlin("jvm") version embeddedKotlinVersion 6 | } 7 | 8 | dependencies { 9 | implementation(platform("org.jetbrains.kotlin:kotlin-bom:2.1.21")) 10 | 11 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21") 12 | implementation("org.jetbrains.kotlin:kotlin-serialization:2.1.21") 13 | 14 | implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.8") 15 | implementation("org.jmailen.gradle:kotlinter-gradle:5.1.0") 16 | 17 | implementation(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2")) 18 | implementation(("org.jetbrains.kotlinx:kotlinx-coroutines-core")) 19 | } 20 | 21 | tasks.withType().configureEach { 22 | compilerOptions { 23 | freeCompilerArgs.addAll( 24 | "-opt-in=kotlin.time.ExperimentalTime", 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /buildSrc/repositories.settings.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.net.URI 2 | 3 | // A settings.gradle.kts plugin for defining shared repositories used by both buildSrc and the root project 4 | 5 | @Suppress("UnstableApiUsage") // centralised repository definitions are incubating 6 | dependencyResolutionManagement { 7 | repositories { 8 | mavenCentral() 9 | gradlePluginPortal() 10 | 11 | maven { 12 | url = URI("https://bindings.krzeminski.it/") 13 | } 14 | 15 | // It has to be defined here because preferring repositories config in settings apparently removes the below 16 | // additions done by Kotlin/JS plugin. 17 | // Tracked in https://youtrack.jetbrains.com/issue/KT-51379 18 | ivy { 19 | name = "Node distributions" 20 | url = URI("https://nodejs.org/dist") 21 | patternLayout { 22 | artifact("v[revision]/[artifact](-v[revision]-[classifier]).[ext]") 23 | } 24 | metadataSources { artifact() } 25 | content { includeModule("org.nodejs", "node") } 26 | } 27 | ivy { 28 | name = "Yarn distributions" 29 | url = URI("https://github.com/yarnpkg/yarn/releases/download") 30 | patternLayout { 31 | artifact("v[revision]/[artifact](-v[revision]).[ext]") 32 | } 33 | metadataSources { artifact() } 34 | content { includeModule("com.yarnpkg", "yarn") } 35 | } 36 | } 37 | 38 | pluginManagement { 39 | repositories { 40 | gradlePluginPortal() 41 | mavenCentral() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "buildSrc" 2 | 3 | apply(from = "./repositories.settings.gradle.kts") 4 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/buildsrc/convention/duplicate-versions.gradle.kts: -------------------------------------------------------------------------------- 1 | package buildsrc.convention 2 | 3 | import org.gradle.api.Task 4 | import org.gradle.kotlin.dsl.registering 5 | import org.gradle.kotlin.dsl.getValue 6 | 7 | val validateDuplicatedVersion by tasks.registering(Task::class) { 8 | // These properties of a project need to be resolved before the 'doLast' block because otherwise, Gradle 9 | // configuration cache cannot be used. 10 | val rootDir = rootDir 11 | val version = version 12 | 13 | doLast { 14 | require( 15 | rootDir.resolve("github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/docsnippets/GettingStartedSnippets.kt").readText() 16 | .contains("\"io.github.typesafegithub:github-workflows-kt:$version\"") 17 | ) { "Library version stated in github-workflows-kt/src/test/.../GettingStarted.kt should be equal to $version!" } 18 | require( 19 | rootDir.resolve(".github/workflows/end-to-end-tests.main.kts").readText() 20 | .contains("\"io.github.typesafegithub:github-workflows-kt:$version\"") 21 | ) { "Library version stated in end-to-end-tests.main.kts should be equal to $version!" } 22 | require( 23 | rootDir.resolve(".github/workflows/end-to-end-tests.main.kts").readText() 24 | .contains("\"io.github.typesafegithub:action-updates-checker:$version\"") 25 | ) { "Library version stated in end-to-end-tests.main.kts should be equal to $version!" } 26 | require( 27 | rootDir.resolve("images/teaser-with-newest-version.svg").readText() 28 | .contains("\"io.github.typesafegithub:github-workflows-kt:$version\"") 29 | ) { "Library version stated in the teaser image shiuld be equal to $version!" } 30 | } 31 | } 32 | 33 | project.tasks.getByName("check") { 34 | dependsOn(validateDuplicatedVersion) 35 | } 36 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/buildsrc/convention/kotlin-jvm-server.gradle.kts: -------------------------------------------------------------------------------- 1 | package buildsrc.convention 2 | 3 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 4 | 5 | plugins { 6 | kotlin("jvm") 7 | `java-library` 8 | 9 | // Code quality. 10 | id("org.jmailen.kotlinter") 11 | } 12 | 13 | dependencies { 14 | testImplementation(platform("io.kotest:kotest-bom:5.9.1")) 15 | testImplementation("io.kotest:kotest-assertions-core") 16 | testImplementation("io.kotest:kotest-runner-junit5") 17 | } 18 | 19 | java { 20 | withJavadocJar() 21 | withSourcesJar() 22 | 23 | toolchain { 24 | requiredJdkVersion() 25 | } 26 | } 27 | 28 | kotlin { 29 | jvmToolchain { 30 | requiredJdkVersion() 31 | } 32 | } 33 | 34 | fun JavaToolchainSpec.requiredJdkVersion() { 35 | languageVersion.set(JavaLanguageVersion.of(17)) 36 | } 37 | 38 | tasks.withType { 39 | kotlinOptions { 40 | jvmTarget = "17" 41 | 42 | allWarningsAsErrors = true 43 | } 44 | 45 | compilerOptions { 46 | freeCompilerArgs.addAll( 47 | "-opt-in=kotlin.ExperimentalStdlibApi", 48 | "-opt-in=kotlin.time.ExperimentalTime", 49 | ) 50 | } 51 | } 52 | 53 | tasks.withType().configureEach { 54 | useJUnitPlatform() 55 | } 56 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/buildsrc/convention/kotlin-jvm.gradle.kts: -------------------------------------------------------------------------------- 1 | package buildsrc.convention 2 | 3 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 4 | 5 | plugins { 6 | kotlin("jvm") 7 | `java-library` 8 | 9 | // Code quality. 10 | id("org.jmailen.kotlinter") 11 | } 12 | 13 | dependencies { 14 | testImplementation(platform("io.kotest:kotest-bom:5.9.1")) 15 | testImplementation("io.kotest:kotest-assertions-core") 16 | testImplementation("io.kotest:kotest-runner-junit5") 17 | } 18 | 19 | java { 20 | withJavadocJar() 21 | withSourcesJar() 22 | 23 | toolchain { 24 | requiredJdkVersion() 25 | } 26 | } 27 | 28 | kotlin { 29 | jvmToolchain { 30 | requiredJdkVersion() 31 | } 32 | } 33 | 34 | fun JavaToolchainSpec.requiredJdkVersion() { 35 | languageVersion.set(JavaLanguageVersion.of(11)) 36 | } 37 | 38 | tasks.withType { 39 | kotlinOptions { 40 | // It's available without extra setup on GitHub Actions runners. 41 | jvmTarget = "11" 42 | 43 | allWarningsAsErrors = true 44 | } 45 | 46 | compilerOptions { 47 | freeCompilerArgs.addAll( 48 | "-opt-in=kotlin.ExperimentalStdlibApi", 49 | "-opt-in=kotlin.time.ExperimentalTime", 50 | ) 51 | } 52 | } 53 | 54 | tasks.withType().configureEach { 55 | useJUnitPlatform() 56 | } 57 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/buildsrc/convention/publishing.gradle.kts: -------------------------------------------------------------------------------- 1 | package buildsrc.convention 2 | 3 | import buildsrc.tasks.AwaitMavenCentralDeployTask 4 | 5 | plugins { 6 | `maven-publish` 7 | signing 8 | } 9 | 10 | val githubUser = "typesafegithub" 11 | val repositoryName = "github-workflows-kt" 12 | val mavenLibraryName = project.name 13 | 14 | publishing { 15 | publications { 16 | create("mavenJava") { 17 | artifactId = mavenLibraryName 18 | from(components["java"]) 19 | 20 | pom { 21 | name.set(mavenLibraryName) 22 | description.set("Authoring GitHub Actions workflows in Kotlin.") 23 | url.set("https://github.com/$githubUser/$repositoryName") 24 | 25 | licenses { 26 | license { 27 | name.set("Apache License, version 2.0") 28 | url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") 29 | } 30 | } 31 | 32 | scm { 33 | connection.set("scm:git:git://github.com/$githubUser/$repositoryName.git/") 34 | developerConnection.set("scm:git:ssh://github.com:$githubUser/$repositoryName.git") 35 | url.set("https://github.com/$githubUser/$repositoryName.git") 36 | } 37 | 38 | developers { 39 | developer { 40 | id.set(githubUser) 41 | name.set("Piotr Krzemiński") 42 | email.set("git@krzeminski.it") 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | signing { 51 | setRequired({ 52 | !project.version.toString().endsWith("-SNAPSHOT") 53 | }) 54 | sign(publishing.publications["mavenJava"]) 55 | 56 | val signingKey = System.getenv("SIGNING_KEY") 57 | val signingPassword = System.getenv("SIGNING_PASSWORD") 58 | useInMemoryPgpKeys(signingKey, signingPassword) 59 | } 60 | 61 | val waitUntilLibraryPresentInMavenCentral by tasks.registering(AwaitMavenCentralDeployTask::class) { 62 | this.groupId.set(project.group.toString()) 63 | this.artifactId.set(mavenLibraryName) 64 | this.version.set(project.version.toString()) 65 | } 66 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/buildsrc/tasks/AwaitMavenCentralDeployTask.kt: -------------------------------------------------------------------------------- 1 | package buildsrc.tasks 2 | 3 | import java.net.URI 4 | import java.net.http.HttpClient 5 | import java.net.http.HttpRequest 6 | import java.net.http.HttpResponse 7 | import kotlinx.coroutines.delay 8 | import kotlinx.coroutines.runBlocking 9 | import org.gradle.api.DefaultTask 10 | import org.gradle.api.provider.Property 11 | import org.gradle.api.publish.plugins.PublishingPlugin 12 | import org.gradle.api.tasks.Input 13 | import org.gradle.api.tasks.TaskAction 14 | import kotlin.time.Duration.Companion.minutes 15 | 16 | abstract class AwaitMavenCentralDeployTask : DefaultTask() { 17 | 18 | @get:Input 19 | abstract val groupId: Property 20 | @get:Input 21 | abstract val artifactId: Property 22 | @get:Input 23 | abstract val version: Property 24 | 25 | init { 26 | group = PublishingPlugin.PUBLISH_TASK_GROUP 27 | } 28 | 29 | @TaskAction 30 | fun awaitDeployment(): Unit = runBlocking { 31 | val queriedUrl = "https://repo.maven.apache.org/maven2/" + 32 | groupId.get().replace(".", "/") + 33 | "/${artifactId.get()}" + 34 | "/${version.get()}" + 35 | "/${artifactId.get()}-${version.get()}.pom" 36 | logger.lifecycle("Querying URL: $queriedUrl") 37 | 38 | while (!isPresent(queriedUrl)) { 39 | logger.lifecycle("Library still not present...") 40 | delay(1.minutes) 41 | } 42 | 43 | if (isPresent(queriedUrl)) { 44 | logger.lifecycle("Library present!") 45 | } 46 | } 47 | 48 | private fun isPresent(queriedUrl: String): Boolean { 49 | val request = HttpRequest.newBuilder() 50 | .uri(URI(queriedUrl)) 51 | .GET() 52 | .build() 53 | val response = HttpClient.newHttpClient() 54 | .send(request, HttpResponse.BodyHandlers.ofString()) 55 | return response.statusCode() != 404 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /code-generator/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | buildsrc.convention.`kotlin-jvm` 3 | 4 | kotlin("plugin.serialization") 5 | } 6 | 7 | dependencies { 8 | implementation("com.google.devtools.ksp:symbol-processing-api:2.1.21-2.0.1") 9 | implementation("com.squareup:kotlinpoet:2.2.0") 10 | implementation("com.squareup:kotlinpoet-ksp:2.2.0") 11 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") 12 | } 13 | -------------------------------------------------------------------------------- /code-generator/src/main/kotlin/io/github/typesafegithub/workflows/codegenerator/KspGenerator.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.codegenerator 2 | 3 | import com.google.devtools.ksp.processing.CodeGenerator 4 | import com.google.devtools.ksp.processing.Resolver 5 | import com.google.devtools.ksp.processing.SymbolProcessor 6 | import com.google.devtools.ksp.symbol.KSAnnotated 7 | import io.github.typesafegithub.workflows.dsl.expressions.generateEventPayloads 8 | 9 | class KspGenerator( 10 | private val codeGenerator: CodeGenerator, 11 | ) : SymbolProcessor { 12 | private var wasRun = false 13 | 14 | override fun process(resolver: Resolver): List { 15 | if (!wasRun) { 16 | generateEventPayloads(codeGenerator = codeGenerator) 17 | wasRun = true 18 | } 19 | return emptyList() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /code-generator/src/main/kotlin/io/github/typesafegithub/workflows/codegenerator/KspGeneratorProvider.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.codegenerator 2 | 3 | import com.google.devtools.ksp.processing.SymbolProcessor 4 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment 5 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 6 | 7 | class KspGeneratorProvider : SymbolProcessorProvider { 8 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = 9 | KspGenerator( 10 | codeGenerator = environment.codeGenerator, 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /code-generator/src/main/kotlin/io/github/typesafegithub/workflows/dsl/expressions/TextUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.dsl.expressions 2 | 3 | import java.util.Locale 4 | 5 | internal fun String.toPascalCase(): String { 6 | val hasOnlyUppercases = none { it in 'a'..'z' } 7 | val normalizedString = if (hasOnlyUppercases) lowercase() else this 8 | return normalizedString 9 | .replace("+", "-plus-") 10 | .split("-", "_", " ", ".", "/") 11 | .joinToString("") { 12 | it.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } 13 | } 14 | } 15 | 16 | internal fun String.toCamelCase(): String = toPascalCase().replaceFirstChar { it.lowercase() } 17 | -------------------------------------------------------------------------------- /code-generator/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider: -------------------------------------------------------------------------------- 1 | io.github.typesafegithub.workflows.codegenerator.KspGeneratorProvider -------------------------------------------------------------------------------- /code-generator/src/test/kotlin/io/github/typesafegithub/workflows/dsl/expressions/TextUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.dsl.expressions 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class TextUtilsTest : 7 | FunSpec({ 8 | context("toPascalCase") { 9 | listOf( 10 | "some-name" to "SomeName", 11 | "some_name" to "SomeName", 12 | "SOME_PROPERTY" to "SomeProperty", 13 | "some+name" to "SomePlusName", 14 | "some name with spaces" to "SomeNameWithSpaces", 15 | "some.name.with.dots" to "SomeNameWithDots", 16 | "some-action/some-subdirectory" to "SomeActionSomeSubdirectory", 17 | ).forEach { (input, output) -> 18 | test("should convert '$input' to '$output'") { 19 | input.toPascalCase() shouldBe output 20 | } 21 | } 22 | } 23 | 24 | context("toCamelCase") { 25 | listOf( 26 | "some-name" to "someName", 27 | "some_name" to "someName", 28 | "some+name" to "somePlusName", 29 | "SOME_NAME" to "someName", 30 | ).forEach { (input, output) -> 31 | test("should convert '$input' to '$output'") { 32 | input.toCamelCase() shouldBe output 33 | } 34 | } 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /docs/Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 46 | -------------------------------------------------------------------------------- /docs/feature-coverage.md: -------------------------------------------------------------------------------- 1 | Here's a list of GitHub Actions features supported by the library, and known to be unsupported yet. 2 | 3 | Legend: 4 | 5 | * ✅ - fully supported 6 | * ✅/❌ - partially supported 7 | * ❌ - not supported 8 | 9 | | Feature | Support | Tracking issue | 10 | |---------------------------------------------|---------|----------------------------------------------------------------------------| 11 | | Conditions | ✅ | | 12 | | Continue on error | ✅ | | 13 | | Concurrency | ✅ | | 14 | | Dependent jobs | ✅ | | 15 | | Different types of triggers | ✅ | | 16 | | Different types of workers | ✅ | | 17 | | Environment variables (`env` context) | ✅ | | 18 | | `github` context | ✅ | | 19 | | Job containers | ✅ | | 20 | | Job environments | ✅ | | 21 | | Docker actions | ✅ | | 22 | | Local actions | ✅ | | 23 | | `outcome` context | ✅ | | 24 | | Permissions | ✅ | | 25 | | Public actions | ✅ | | 26 | | `runner` context | ✅ | | 27 | | Strategy matrix (`matrix` context) | ✅/❌ | [#368](https://github.com/typesafegithub/github-workflows-kt/issues/368) | 28 | | Secrets (`secrets` context) | ✅ | | 29 | | Service containers | ✅ | | 30 | | Timeouts | ✅ | | 31 | | Workflow dispatch inputs (`inputs` context) | ✅/❌ | [#811](https://github.com/typesafegithub/github-workflows-kt/issues/811) | 32 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # github-workflows-kt 2 | 3 | github-workflows-kt is a tool for generating 4 | [GitHub Actions workflow](https://docs.github.com/en/actions/using-workflows) YAML files in a **type-safe** script, helping you to 5 | build **robust** workflows for your GitHub projects without mistakes, with **pleasure**, in 6 | [Kotlin](https://kotlinlang.org/). 7 | 8 | > _You won't go back to YAML!_ 9 | 10 | ## 💡 Idea 11 | 12 | We're often surrounded by YAML configuration. It's a powerful format that provides simple syntax for defining 13 | hierarchical data, but it is sometimes used (abused?) to configure complicated scenarios which leads to complicated 14 | files that are difficult to write and maintain. 15 | 16 | Who among us hasn't accidentally used the wrong indentation, missed a possibility to extract a reusable piece of code, 17 | or been confused by ambiguous types? The power of a generic-purpose programming language would come in handy in these 18 | cases. 19 | 20 | We're developing **github-workflows-kt** to solve these and other problems, so you can create GitHub Workflows with 21 | confidence. 22 | 23 | ## ✨ Benefits 24 | 25 | * **no indentation confusion** - Kotlin's syntax doesn't rely on it 26 | * **immediate validation** - catch bugs early during development, not during runtime 27 | * **strongly typed values** - no more confusion about what type is needed for a given parameter 28 | * **superb IDE support** - author your workflows in any IDE that supports Kotlin, with auto-completion and documentation 29 | at your fingertips 30 | * **no duplication** - don't repeat yourself! Share common configuration using constant values, or define your own 31 | functions to encapsulate logic 32 | * **fully featured language** - use the full power of Kotlin to generate workflows dynamically, randomly generate data, 33 | or add custom validation. Defining workflow logic in Kotlin is currently experimental 34 | * **type-safe action bindings** - possible to use every action using auto-generated Kotlin bindings 35 | * integrates with [github-actions-typing](https://github.com/typesafegithub/github-actions-typing) to use typings 36 | provided by action authors 37 | * and more! 38 | -------------------------------------------------------------------------------- /docs/projects-using-this-library.md: -------------------------------------------------------------------------------- 1 | # Projects using this library 2 | 3 | * [aSoft-Ltd/oss](https://github.com/aSoft-Ltd/oss/blob/main/.github/workflows/) 4 | * [ComposeOClock](https://github.com/Splitties/ComposeOClock/tree/main/.github/workflows) 5 | * [course-evals](https://github.com/opLetter/course-evals/tree/master/.github/workflows) 6 | * [factcast](https://github.com/factcast/factcast/tree/master/.github/kts) 7 | * [github-actions-typing](https://github.com/typesafegithub/github-actions-typing/tree/main/.github/workflows) 8 | * [github-workflows-kt](https://github.com/typesafegithub/github-workflows-kt/tree/main/.github/workflows) (this library - we [dogfood](https://en.wikipedia.org/wiki/Eating_your_own_dog_food), of course) 9 | * [gradle-release-plugin](https://github.com/cloudshiftinc/gradle-release-plugin/tree/main/.github/workflows) 10 | * [jEdit](https://github.com/jEdit-editor/jEdit/tree/master/.github/workflows) 11 | * [Jopiter](https://github.com/JopiterApp/jopiter-backend/tree/master/.github/workflows) 12 | * [markout](https://github.com/mfwgenerics/markout/tree/main/markout-github-workflows-kt) 13 | * [Petals](https://github.com/LeoColman/Petals/tree/main/.github/workflows) 14 | * [rankquest-studio](https://github.com/jillesvangurp/rankquest-studio/tree/main/.github/workflows) 15 | * [setup-wsl](https://github.com/Vampire/setup-wsl/tree/master/.github/workflows) 16 | * [snakeyaml-engine-kmp](https://github.com/krzema12/snakeyaml-engine-kmp/tree/main/.github/workflows) 17 | * [Spock](https://github.com/spockframework/spock/tree/master/.github/workflows) 18 | * [spring-cqs](https://github.com/prisma-capacity/spring-cqs/tree/main/.github/kts) 19 | * [twitch-announcement-discord-bot](https://github.com/NikkyAI/twitch-announcement-discord-bot/tree/main/.github/workflows) 20 | * [WalletConnectKotlinV2](https://github.com/WalletConnect/WalletConnectKotlinV2/tree/develop/.github/workflows/scripts) 21 | * [WiertarBot](https://github.com/kugo12/WiertarBot/tree/main/.github/workflows) 22 | * [xenosearch](https://github.com/krzema12/xenosearch/tree/main/.github/workflows) 23 | * feel free to add your project here! 24 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.6.1 2 | mkdocs-material==9.6.14 3 | mkdocs-material-extensions==1.3.1 4 | mkdocs-video==1.5.0 5 | pymdown-extensions==10.15 6 | -------------------------------------------------------------------------------- /docs/user-guide/compensating-librarys-missing-features.md: -------------------------------------------------------------------------------- 1 | # Compensating library's missing features 2 | 3 | You may find yourself willing to use GitHub Actions' feature that is not yet reflected in this library, neither in the 4 | core workflows/jobs/steps API, nor in the action bindings. We've thought about it. The library provides several points 5 | of extension so that you can keep using it, and in the meantime report the missing feature to us so that we can add it 6 | to one of the next releases. See the below sections to find your specific case. 7 | 8 | The general approach is that whatever is overridden/customized using the below approaches, takes the precedence over 9 | built-in arguments. 10 | 11 | ## Workflows, jobs and steps 12 | 13 | They have an extra argument - `_customArguments` - which is a map from `String` to whatever values or collections are 14 | needed, especially using basic types like booleans, strings or integers, and further nesting of maps and lists. 15 | 16 | For example: 17 | 18 | ```kotlin 19 | --8<-- "CompensatingLibrarysMissingFeaturesSnippets.kt:custom-arguments-1" 20 | --8<-- "CompensatingLibrarysMissingFeaturesSnippets.kt:custom-arguments-2" 21 | ``` 22 | 23 | ## Action's inputs 24 | 25 | Each action binding has an extra constructor parameter - `_customInputs` - which is a map from `String` to `String`: 26 | 27 | ```kotlin 28 | --8<-- "CompensatingLibrarysMissingFeaturesSnippets.kt:custom-inputs-1" 29 | --8<-- "CompensatingLibrarysMissingFeaturesSnippets.kt:custom-inputs-2" 30 | ``` 31 | 32 | You can use it to set inputs that the binding doesn't know about, or to set any custom value if the binding's typing is 33 | incorrect or faulty. 34 | 35 | ## Action's version 36 | 37 | Each action binding has an extra constructor parameter - `_customVersion` - which is a string overriding action's 38 | version: 39 | 40 | ```kotlin 41 | --8<-- "CompensatingLibrarysMissingFeaturesSnippets.kt:custom-version-1" 42 | --8<-- "CompensatingLibrarysMissingFeaturesSnippets.kt:custom-version-2" 43 | ``` 44 | 45 | It's useful e.g. when the binding doesn't keep up with action's versions and the API is fairly compatible, or if you 46 | want to use a specific minor version. 47 | 48 | ## I still cannot customize what I need 49 | 50 | Well, it means we missed something - sorry for that! Please report it via GitHub issues. 51 | -------------------------------------------------------------------------------- /docs/user-guide/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | As an exercise, we'll add a job that prints out `Hello world!`. Feel free to replace the actual workflow's logic and all 4 | names with your own. 5 | 6 | 1. Install Kotlin as a stand-alone binary, e.g. from Snap Store when on Linux: 7 | ``` 8 | sudo snap install kotlin --classic 9 | ``` 10 | Make sure this is the newest version available. Kotlin scripting still has some rough edges, and improvements 11 | are introduced with each new Kotlin release. 12 | Also make sure that you use Java 11+. 13 | 2. Create a new executable file in your repository: 14 | ``` 15 | touch .github/workflows/hello_world_workflow.main.kts 16 | chmod +x .github/workflows/hello_world_workflow.main.kts 17 | ``` 18 | This location is not a hard requirement, it's just recommended for consistency with enforced location of actual 19 | GitHub Actions workflows. 20 | 3. Put this content into the previously created file and save it: 21 | ```kotlin 22 | --8<-- "GettingStartedSnippets.kt:getting-started-1" 23 | --8<-- "GettingStartedSnippets.kt:getting-started-2" 24 | --8<-- "GettingStartedSnippets.kt:getting-started-3" 25 | ``` 26 | This way we create a workflow with the DSL provided by this library. The reason it needs source 27 | file path is to be able to generate consistency checks, to ensure that both source and target files are in sync. 28 | You'll see it in a moment in the generated file. 29 | 4. Generate the YAML by calling the above script: 30 | ``` 31 | .github/workflows/hello_world_workflow.main.kts 32 | ``` 33 | It can be also executed straight from IntelliJ, by clicking the green ▶️ button next to the shebang. 34 | Notice that there's an extra job generated by the library that regenerates the YAML in job's runtime and ensures that 35 | it's equal to the YAML committed to the repository. 36 | 5. Commit both files, push the changes to GitHub and make sure the workflow is green when ran on GitHub Actions. 37 | 6. Last but not least, feel invited to join the [Slack channel](https://kotlinlang.slack.com/archives/C02UUATR7RC/) 38 | dedicated to this library. You'll find information about upcoming breaking changes, discussion about new features, 39 | and more! If you don't know how to sign up to the Kotlin's Slack space, see [here](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up). 40 | -------------------------------------------------------------------------------- /docs/user-guide/job-outputs.md: -------------------------------------------------------------------------------- 1 | # Job outputs 2 | 3 | It's possible to pass output from a job in a somewhat type-safe way (that is: types aren't checked, but the field names 4 | are). 5 | 6 | First, define `outputs` parameter in `job` function, inheriting from `JobOutputs`: 7 | 8 | ```kotlin 9 | --8<-- "JobOutputsSnippets.kt:define-job-outputs-1" 10 | --8<-- "JobOutputsSnippets.kt:define-job-outputs-2" 11 | ``` 12 | 13 | To set an output from within the job, use `jobOutputs`, and then an appropriate object field: 14 | 15 | ```kotlin 16 | --8<-- "JobOutputsSnippets.kt:set-job-outputs" 17 | ``` 18 | 19 | and then use job's output from another job this way: 20 | 21 | ```kotlin 22 | --8<-- "JobOutputsSnippets.kt:use-job-outputs" 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/user-guide/nightly-builds.md: -------------------------------------------------------------------------------- 1 | # Nightly builds 2 | 3 | Sometimes you may want to test a change that has been already merged to `main`, but not yet officially released. In this 4 | case, you can use snapshots published for each commit to the `main` branch. 5 | 6 | To use a given "snapshot" version, e.g. `1.3.2-SNAPSHOT`, replace your scripts' preamble with: 7 | 8 | ```kotlin 9 | @file:Repository("https://s01.oss.sonatype.org/content/repositories/snapshots/") 10 | @file:DependsOn("io.github.typesafegithub:github-workflows-kt:1.3.2-SNAPSHOT") 11 | ``` 12 | 13 | Remember that requesting version `1.3.2-SNAPSHOT`, if it's being actively developed, may return a different build of the 14 | library each time it's requested. It can also happen occasionally that the snapshot doesn't correspond to any commit on 15 | the `main` branch, and instead some PR that wasn't yet merged. That's why the snapshots are meant to quickly check 16 | something simple that wasn't yet released, not for depending on such unstable version constantly. 17 | -------------------------------------------------------------------------------- /github-workflows-kt/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | import org.jmailen.gradle.kotlinter.tasks.ConfigurableKtLintTask 3 | 4 | plugins { 5 | buildsrc.convention.`kotlin-jvm` 6 | buildsrc.convention.publishing 7 | buildsrc.convention.`duplicate-versions` 8 | 9 | kotlin("plugin.serialization") 10 | id("com.google.devtools.ksp") version "2.1.21-2.0.1" 11 | 12 | // Code quality. 13 | id("io.gitlab.arturbosch.detekt") 14 | id("info.solidsoft.pitest") version "1.15.0" 15 | id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.17.0" 16 | 17 | id("org.jetbrains.dokka") version "2.0.0" 18 | } 19 | 20 | group = rootProject.group 21 | version = rootProject.version 22 | 23 | dependencies { 24 | implementation("it.krzeminski:snakeyaml-engine-kmp:3.1.1") 25 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.1") 26 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") 27 | implementation(projects.sharedInternal) 28 | ksp(projects.codeGenerator) 29 | 30 | testImplementation("dev.zacsweers.kctfork:core:0.7.1") 31 | // Needed to use the right version of the compiler for the libraries that depend on it. 32 | testImplementation(kotlin("compiler")) 33 | testImplementation(kotlin("reflect")) 34 | 35 | // GitHub action bindings 36 | testImplementation("actions:checkout:v4") 37 | testImplementation("actions:deploy-pages:v4") 38 | testImplementation("actions:setup-java:v4") 39 | testImplementation("actions:upload-artifact:v3") 40 | testImplementation("aws-actions:configure-aws-credentials:v4") 41 | testImplementation("EndBug:add-and-commit:v9") 42 | } 43 | 44 | tasks.withType { 45 | compilerOptions { 46 | freeCompilerArgs.addAll( 47 | "-opt-in=io.github.typesafegithub.workflows.internal.InternalGithubActionsApi", 48 | ) 49 | } 50 | } 51 | 52 | tasks.test { 53 | // The integration tests read from and write to there. 54 | inputs.dir("$rootDir/.github/workflows") 55 | 56 | // It's a workaround to be able to use action bindings provided by the server. They declare a dependency on 57 | // github-workflows-kt, and I think it causes some kind of version clash (e.g. between 2.3.0 and 2.3.1-SNAPSHOT). 58 | dependsOn(tasks.jar) 59 | } 60 | 61 | kotlin { 62 | explicitApi() 63 | } 64 | 65 | fun ConfigurableKtLintTask.kotlinterConfig() { 66 | exclude { it.file.invariantSeparatorsPath.contains("/generated/") } 67 | } 68 | 69 | tasks.lintKotlinMain { 70 | kotlinterConfig() 71 | } 72 | tasks.formatKotlinMain { 73 | kotlinterConfig() 74 | } 75 | 76 | pitest { 77 | junit5PluginVersion.set("1.1.0") 78 | } 79 | 80 | dokka { 81 | moduleName.set("github-workflows-kt") 82 | } 83 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/annotations/ExperimentalKotlinLogicStep.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.annotations 2 | 3 | @Target(AnnotationTarget.FUNCTION) 4 | @RequiresOptIn 5 | public annotation class ExperimentalKotlinLogicStep 6 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/AbstractResult.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain 2 | 3 | public abstract class AbstractResult internal constructor( 4 | private val value: String, 5 | ) { 6 | public infix fun eq(status: Status): String = "$value == $status" 7 | 8 | public infix fun neq(status: Status): String = "$value != $status" 9 | 10 | override fun toString(): String = value 11 | 12 | public enum class Status { 13 | Success, 14 | Failure, 15 | Cancelled, 16 | Skipped, 17 | ; 18 | 19 | override fun toString(): String = "'${name.lowercase()}'" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/Concurrency.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * Using concurrency allows running single workflow/job at a time 8 | * See https://docs.github.com/en/actions/using-jobs/using-concurrency 9 | * See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idconcurrency 10 | */ 11 | @Serializable 12 | public data class Concurrency( 13 | val group: String, 14 | @SerialName("cancel-in-progress") 15 | val cancelInProgress: Boolean = false, 16 | ) 17 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/Environment.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain 2 | 3 | /** 4 | * https://docs.github.com/en/actions/using-jobs/using-environments-for-jobs 5 | */ 6 | public data class Environment( 7 | val name: String, 8 | val url: String? = null, 9 | ) 10 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/Job.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain 2 | 3 | import io.github.typesafegithub.workflows.dsl.HasCustomArguments 4 | import io.github.typesafegithub.workflows.validation.requireMatchesRegex 5 | import kotlinx.serialization.Contextual 6 | 7 | public data class Job( 8 | val id: String, 9 | val name: String? = null, 10 | val runsOn: RunnerType, 11 | val steps: List>, 12 | val needs: List> = emptyList(), 13 | val outputs: OUTPUT, 14 | val env: Map = mapOf(), 15 | val condition: String? = null, 16 | val strategyMatrix: Map>? = null, 17 | val permissions: Map? = null, 18 | val timeoutMinutes: Int? = null, 19 | val concurrency: Concurrency? = null, 20 | val container: Container? = null, 21 | val environment: Environment? = null, 22 | val services: Map = emptyMap(), 23 | override val _customArguments: Map = mapOf(), 24 | ) : HasCustomArguments { 25 | init { 26 | requireMatchesRegex( 27 | field = "Job.id", 28 | value = id, 29 | regex = Regex("[a-zA-Z_][a-zA-Z0-9_-]*"), 30 | url = "https://docs.github.com/en/actions/using-jobs/using-jobs-in-a-workflow#setting-an-id-for-a-job", 31 | ) 32 | timeoutMinutes?.let { value -> 33 | require(value > 0) { "timeout should be positive" } 34 | } 35 | outputs.job = this 36 | } 37 | 38 | public inner class Result : AbstractResult("needs.$id.result") 39 | 40 | public val result: Result get() = Result() 41 | } 42 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/JobOutputs.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain 2 | 3 | import io.github.typesafegithub.workflows.dsl.expressions.expr 4 | import kotlin.properties.ReadWriteProperty 5 | import kotlin.reflect.KProperty 6 | 7 | public open class JobOutputs { 8 | internal lateinit var job: Job<*> 9 | private val _outputMapping: MutableMap = mutableMapOf() 10 | 11 | public object EMPTY : JobOutputs() 12 | 13 | public val outputMapping: Map get() = _outputMapping.toMap() 14 | 15 | public fun output(): Ref = Ref() 16 | 17 | public inner class Ref : ReadWriteProperty { 18 | private var initialized: Boolean = false 19 | 20 | override fun getValue( 21 | thisRef: JobOutputs, 22 | property: KProperty<*>, 23 | ): String { 24 | val key = property.name 25 | check(initialized) { 26 | "output '$key' must be initialized" 27 | } 28 | return "needs.${job.id}.outputs.$key" 29 | } 30 | 31 | override fun setValue( 32 | thisRef: JobOutputs, 33 | property: KProperty<*>, 34 | value: String, 35 | ) { 36 | val key = property.name 37 | check(!initialized) { 38 | "Value for output '$key' can be assigned only once!" 39 | } 40 | _outputMapping[key] = expr(value) 41 | initialized = true 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/RunnerType.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain 2 | 3 | /** 4 | * Types of GitHub-hosted runners available. Should be kept in sync with the official list at 5 | * https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#choosing-github-hosted-runners 6 | */ 7 | public sealed interface RunnerType { 8 | // Custom runner. Could be an expression `runsOn = expr("github.event.inputs.run-on")` 9 | public data class Custom( 10 | val runsOn: String, 11 | ) : RunnerType 12 | 13 | public data class Labelled( 14 | val labels: Set, 15 | ) : RunnerType { 16 | public constructor(vararg labels: String) : this(LinkedHashSet(labels.toList())) 17 | 18 | init { 19 | require(labels.isNotEmpty()) { "At least one label must be provided" } 20 | } 21 | } 22 | 23 | /** 24 | * @see Choosing runners in a group 25 | */ 26 | @Suppress("MaxLineLength") 27 | public data class Group( 28 | val name: String, 29 | val labels: Set? = null, 30 | ) : RunnerType { 31 | public constructor(name: String, vararg labels: String) : this(name, LinkedHashSet(labels.toList())) 32 | } 33 | 34 | // "Latest" labels 35 | public object UbuntuLatest : RunnerType 36 | 37 | public object WindowsLatest : RunnerType 38 | 39 | public object MacOSLatest : RunnerType 40 | 41 | // Windows runners 42 | public object Windows2025 : RunnerType 43 | 44 | public object Windows2022 : RunnerType 45 | 46 | public object Windows2019 : RunnerType 47 | 48 | public object Windows2016 : RunnerType 49 | 50 | // Ubuntu runners 51 | public object Ubuntu2004 : RunnerType 52 | 53 | public object Ubuntu1804 : RunnerType 54 | 55 | // macOS runners 56 | public object MacOS11 : RunnerType 57 | 58 | public object MacOS1015 : RunnerType 59 | 60 | public companion object { 61 | /** 62 | * @see Choosing self-hosted runners 63 | */ 64 | @Suppress("MaxLineLength") 65 | public fun selfHosted(vararg labels: String): Labelled = Labelled("self-hosted", *labels) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/Shell.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain 2 | 3 | public sealed interface Shell { 4 | public data class Custom( 5 | val value: String, 6 | ) : Shell 7 | 8 | public object Bash : Shell 9 | 10 | public object Cmd : Shell 11 | 12 | public object Pwsh : Shell 13 | 14 | public object PowerShell : Shell 15 | 16 | public object Python : Shell 17 | 18 | public object Sh : Shell 19 | } 20 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/Workflow.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain 2 | 3 | import io.github.typesafegithub.workflows.domain.triggers.Trigger 4 | import io.github.typesafegithub.workflows.dsl.HasCustomArguments 5 | import io.github.typesafegithub.workflows.yaml.ConsistencyCheckJobConfig 6 | import kotlinx.serialization.Contextual 7 | import java.io.File 8 | 9 | public data class Workflow( 10 | val name: String, 11 | val on: List, 12 | val env: Map, 13 | val sourceFile: File?, 14 | val targetFileName: String?, 15 | val consistencyCheckJobConfig: ConsistencyCheckJobConfig, 16 | val concurrency: Concurrency? = null, 17 | val permissions: Map? = null, 18 | val jobs: List>, 19 | override val _customArguments: Map = mapOf(), 20 | ) : HasCustomArguments 21 | 22 | /** 23 | * @see GitHub 24 | */ 25 | @Suppress("MaxLineLength") 26 | public enum class Permission( 27 | public val value: String, 28 | ) { 29 | Actions("actions"), 30 | Checks("checks"), 31 | Contents("contents"), 32 | Deployments("deployments"), 33 | Discussions("discussions"), 34 | IdToken("id-token"), 35 | Issues("issues"), 36 | Packages("packages"), 37 | Pages("pages"), 38 | PullRequests("pull-requests"), 39 | RepositoryProjects("repository-projects"), 40 | SecurityEvents("security-events"), 41 | Statuses("statuses"), 42 | } 43 | 44 | /** 45 | * @see GitHub 46 | */ 47 | @Suppress("MaxLineLength") 48 | public enum class Mode( 49 | public val value: String, 50 | ) { 51 | Read("read"), 52 | Write("write"), 53 | None("none"), 54 | } 55 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/actions/Action.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.actions 2 | 3 | import io.github.typesafegithub.workflows.domain.actions.Action.Outputs 4 | import io.github.typesafegithub.workflows.yaml.toYaml 5 | 6 | public abstract class Action { 7 | public abstract fun toYamlArguments(): LinkedHashMap 8 | 9 | public val yamlArgumentsString: String get() = toYamlArguments().toYaml() 10 | 11 | public abstract fun buildOutputObject(stepId: String): OUTPUTS 12 | 13 | public abstract val usesString: String 14 | 15 | public open class Outputs( 16 | private val stepId: String, 17 | ) { 18 | public operator fun get(outputName: String): String = "steps.$stepId.outputs.$outputName" 19 | } 20 | } 21 | 22 | public abstract class RegularAction( 23 | public open val actionOwner: String, 24 | public open val actionName: String, 25 | public open val actionVersion: String, 26 | ) : Action() { 27 | override val usesString: String 28 | get() = "$actionOwner/$actionName@$actionVersion" 29 | } 30 | 31 | public abstract class LocalAction( 32 | public open val actionPath: String, 33 | ) : Action() { 34 | override val usesString: String 35 | get() = actionPath 36 | } 37 | 38 | public abstract class DockerAction( 39 | public open val actionImage: String, 40 | public open val actionTag: String, 41 | public open val actionHost: String? = null, 42 | ) : Action() { 43 | override val usesString: String 44 | get() = "docker://${if (actionHost == null) "" else "$actionHost/"}$actionImage:$actionTag" 45 | } 46 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/actions/CustomAction.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.actions 2 | 3 | import io.github.typesafegithub.workflows.domain.actions.Action.Outputs 4 | 5 | /** 6 | * CustomAction can be used when there is no type-safe binding action 7 | * and a quickly untyped binding is needed to fill the blank. 8 | * 9 | * Consider adding first-class support for your action! See CONTRIBUTING.md. 10 | */ 11 | public data class CustomAction( 12 | override val actionOwner: String, 13 | override val actionName: String, 14 | override val actionVersion: String, 15 | public val inputs: Map = emptyMap(), 16 | ) : RegularAction(actionOwner, actionName, actionVersion) { 17 | override fun toYamlArguments(): LinkedHashMap = LinkedHashMap(inputs) 18 | 19 | override fun buildOutputObject(stepId: String): Outputs = Outputs(stepId) 20 | } 21 | 22 | public data class CustomLocalAction( 23 | override val actionPath: String, 24 | public val inputs: Map = emptyMap(), 25 | ) : LocalAction(actionPath) { 26 | override fun toYamlArguments(): LinkedHashMap = LinkedHashMap(inputs) 27 | 28 | override fun buildOutputObject(stepId: String): Outputs = Outputs(stepId) 29 | } 30 | 31 | public data class CustomDockerAction( 32 | override val actionImage: String, 33 | override val actionTag: String, 34 | public val inputs: Map = emptyMap(), 35 | override val actionHost: String? = null, 36 | ) : DockerAction(actionImage, actionTag, actionHost) { 37 | override fun toYamlArguments(): LinkedHashMap = LinkedHashMap(inputs) 38 | 39 | override fun buildOutputObject(stepId: String): Outputs = Outputs(stepId) 40 | } 41 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/contexts/Contexts.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.contexts 2 | 3 | public data class Contexts( 4 | val github: GithubContext, 5 | ) { 6 | val outputs: OutputsContext = OutputsContext 7 | } 8 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/contexts/GithubContext.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("ConstructorParameterNaming") 2 | 3 | package io.github.typesafegithub.workflows.domain.contexts 4 | 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | public data class GithubContext( 9 | val repository: String, 10 | val sha: String, 11 | val ref: String? = null, 12 | val base_ref: String? = null, 13 | val event: GithubContextEvent, 14 | val event_name: String, 15 | ) 16 | 17 | @Serializable 18 | public data class GithubContextEvent( 19 | val after: String? = null, 20 | ) 21 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/contexts/OutputsContext.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.contexts 2 | 3 | import java.io.File 4 | 5 | public object OutputsContext { 6 | public operator fun set( 7 | outputName: String, 8 | value: String, 9 | ) { 10 | File(System.getenv("GITHUB_OUTPUT")).appendText("$outputName=$value") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/BranchProtectionRule.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | public data class BranchProtectionRule( 8 | override val _customArguments: Map = mapOf(), 9 | ) : Trigger() 10 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/CheckRun.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | public data class CheckRun( 8 | override val _customArguments: Map = mapOf(), 9 | ) : Trigger() 10 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/CheckSuite.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | public data class CheckSuite( 8 | override val _customArguments: Map = mapOf(), 9 | ) : Trigger() 10 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Create.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | public data class Create( 8 | override val _customArguments: Map = mapOf(), 9 | ) : Trigger() 10 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Delete.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | public data class Delete( 8 | override val _customArguments: Map = mapOf(), 9 | ) : Trigger() 10 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Deployment.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#deployment 8 | */ 9 | @Serializable 10 | public data class Deployment( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/DeploymentStatus.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#deployment_status 8 | */ 9 | @Serializable 10 | public data class DeploymentStatus( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Discussion.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | public data class Discussion( 8 | override val _customArguments: Map = mapOf(), 9 | ) : Trigger() 10 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/DiscussionComment.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | public data class DiscussionComment( 8 | override val _customArguments: Map = mapOf(), 9 | ) : Trigger() 10 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Fork.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#fork 8 | */ 9 | @Serializable 10 | public data class Fork( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Gollum.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum 8 | */ 9 | @Serializable 10 | public data class Gollum( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/IssueComment.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment 8 | */ 9 | @Serializable 10 | public data class IssueComment( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Issues.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues 8 | */ 9 | @Serializable 10 | public data class Issues( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Label.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#label 8 | */ 9 | @Serializable 10 | public data class Label( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/MergeGroup.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#merge_group 8 | */ 9 | @Serializable 10 | public data class MergeGroup( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Milestone.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#milestone 8 | */ 9 | @Serializable 10 | public data class Milestone( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/PageBuild.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#page_build 8 | */ 9 | @Serializable 10 | public data class PageBuild( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Project.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#project 8 | */ 9 | @Serializable 10 | public data class Project( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/ProjectCard.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#project 8 | */ 9 | @Serializable 10 | public data class ProjectCard( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/ProjectColumn.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#project_column 8 | */ 9 | @Serializable 10 | public data class ProjectColumn( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/PublicWorkflow.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#public 8 | */ 9 | @Serializable 10 | public data class PublicWorkflow( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/PullRequest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import io.github.typesafegithub.workflows.internal.CaseEnumSerializer 4 | import kotlinx.serialization.Contextual 5 | import kotlinx.serialization.InternalSerializationApi 6 | import kotlinx.serialization.Serializable 7 | 8 | @Serializable 9 | public data class PullRequest( 10 | val types: List = emptyList(), 11 | val branches: List? = null, 12 | val branchesIgnore: List? = null, 13 | val paths: List? = null, 14 | val pathsIgnore: List? = null, 15 | override val _customArguments: Map = mapOf(), 16 | ) : Trigger() { 17 | init { 18 | require(!(branches != null && branchesIgnore != null)) { 19 | "Cannot define both 'branches' and 'branchesIgnore'!" 20 | } 21 | require(!(paths != null && pathsIgnore != null)) { 22 | "Cannot define both 'paths' and 'pathsIgnore'!" 23 | } 24 | } 25 | 26 | @InternalSerializationApi 27 | internal class Serializer : CaseEnumSerializer(Type::class.qualifiedName!!, Type.values()) 28 | 29 | /** 30 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request 31 | */ 32 | @OptIn(InternalSerializationApi::class) 33 | @Serializable(with = Serializer::class) 34 | public enum class Type { 35 | Assigned, 36 | Unassigned, 37 | Labeled, 38 | Unlabeled, 39 | Opened, 40 | Edited, 41 | Closed, 42 | Reopened, 43 | Synchronize, 44 | ConvertedToDraft, 45 | ReadyForReview, 46 | Locked, 47 | Unlocked, 48 | ReviewRequested, 49 | ReviewRequestRemoved, 50 | AutoMergeEnabled, 51 | AutoMergeDisabled, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/PullRequestReview.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_review 8 | */ 9 | @Serializable 10 | public data class PullRequestReview( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/PullRequestReviewComment.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_review_comment 8 | */ 9 | @Serializable 10 | public data class PullRequestReviewComment( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/PullRequestTarget.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import io.github.typesafegithub.workflows.internal.CaseEnumSerializer 4 | import kotlinx.serialization.Contextual 5 | import kotlinx.serialization.InternalSerializationApi 6 | import kotlinx.serialization.Serializable 7 | 8 | // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target 9 | @Serializable 10 | public data class PullRequestTarget( 11 | val types: List = emptyList(), 12 | val branches: List? = null, 13 | val branchesIgnore: List? = null, 14 | val paths: List? = null, 15 | val pathsIgnore: List? = null, 16 | override val _customArguments: Map = mapOf(), 17 | ) : Trigger() { 18 | init { 19 | require(!(branches != null && branchesIgnore != null)) { 20 | "Cannot define both 'branches' and 'branchesIgnore'!" 21 | } 22 | require(!(paths != null && pathsIgnore != null)) { 23 | "Cannot define both 'paths' and 'pathsIgnore'!" 24 | } 25 | } 26 | 27 | @InternalSerializationApi 28 | internal class Serializer : CaseEnumSerializer(Type::class.qualifiedName!!, Type.values()) 29 | 30 | @OptIn(InternalSerializationApi::class) 31 | @Serializable(with = Serializer::class) 32 | public enum class Type { 33 | Assigned, 34 | Unassigned, 35 | Labeled, 36 | Unlabeled, 37 | Opened, 38 | Edited, 39 | Closed, 40 | Reopened, 41 | Synchronize, 42 | ConvertedToDraft, 43 | ReadyForReview, 44 | Locked, 45 | Unlocked, 46 | ReviewRequested, 47 | ReviewRequestRemoved, 48 | AutoMergeEnabled, 49 | AutoMergeDisabled, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Push.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.SerialName 5 | 6 | @kotlinx.serialization.Serializable 7 | public data class Push( 8 | val branches: List? = null, 9 | @SerialName("branches-ignore") 10 | val branchesIgnore: List? = null, 11 | val tags: List? = null, 12 | @SerialName("tags-ignore") 13 | val tagsIgnore: List? = null, 14 | val paths: List? = null, 15 | @SerialName("paths-ignore") 16 | val pathsIgnore: List? = null, 17 | override val _customArguments: Map = mapOf(), 18 | ) : Trigger() { 19 | init { 20 | require(!(branches != null && branchesIgnore != null)) { 21 | "Cannot define both 'branches' and 'branchesIgnore'!" 22 | } 23 | require(!(tags != null && tagsIgnore != null)) { 24 | "Cannot define both 'tags' and 'tagsIgnore'!" 25 | } 26 | require(!(paths != null && pathsIgnore != null)) { 27 | "Cannot define both 'paths' and 'pathsIgnore'!" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/RegistryPackage.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#registry_package 8 | */ 9 | @Serializable 10 | public data class RegistryPackage( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Release.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release 8 | */ 9 | @Serializable 10 | public data class Release( 11 | val types: List? = null, 12 | override val _customArguments: Map = mapOf(), 13 | ) : Trigger() 14 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/RepositoryDispatch.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#repository_dispatch 8 | */ 9 | @Serializable 10 | public data class RepositoryDispatch( 11 | val types: List? = null, 12 | override val _customArguments: Map = mapOf(), 13 | ) : Trigger() 14 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Status.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | public data class Status( 8 | override val _customArguments: Map = mapOf(), 9 | ) : Trigger() 10 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Trigger.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import io.github.typesafegithub.workflows.dsl.HasCustomArguments 4 | 5 | public sealed class Trigger : HasCustomArguments 6 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/Watch.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#watch 8 | */ 9 | @Serializable 10 | public data class Watch( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/WorkflowCall.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | /** 8 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_call 9 | */ 10 | @Serializable 11 | public data class WorkflowCall( 12 | val inputs: Map = emptyMap(), 13 | val outputs: Map? = null, 14 | val secrets: Map? = null, 15 | override val _customArguments: Map = mapOf(), 16 | ) : Trigger() { 17 | @Serializable 18 | public enum class Type { 19 | @SerialName("boolean") 20 | Boolean, 21 | 22 | @SerialName("number") 23 | Number, 24 | 25 | @SerialName("string") 26 | String, 27 | } 28 | 29 | @Serializable 30 | public class Input( 31 | public val description: String, 32 | public val required: Boolean, 33 | public val type: Type, 34 | public val default: String? = null, 35 | ) 36 | 37 | @Serializable 38 | public class Output( 39 | public val description: String, 40 | public val value: String, 41 | ) 42 | 43 | @Serializable 44 | public class Secret( 45 | public val description: String, 46 | public val required: Boolean, 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/WorkflowDispatch.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_dispatchinputs 8 | @Serializable 9 | public data class WorkflowDispatch( 10 | val inputs: Map = emptyMap(), 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() { 13 | @Serializable 14 | public enum class Type { 15 | @SerialName("choice") 16 | Choice, 17 | 18 | @SerialName("environment") 19 | Environment, 20 | 21 | @SerialName("boolean") 22 | Boolean, 23 | 24 | @SerialName("number") 25 | Number, 26 | 27 | @SerialName("string") 28 | String, 29 | } 30 | 31 | @Serializable 32 | public class Input( 33 | public val description: String, 34 | public val required: Boolean, 35 | public val type: Type, 36 | public val options: List = emptyList(), 37 | public val default: String? = null, 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/domain/triggers/WorkflowRun.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import kotlinx.serialization.Contextual 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run 8 | */ 9 | @Serializable 10 | public data class WorkflowRun( 11 | override val _customArguments: Map = mapOf(), 12 | ) : Trigger() 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/Container.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.dsl 2 | 3 | import io.github.typesafegithub.workflows.domain.PortMapping 4 | import io.github.typesafegithub.workflows.domain.PortMapping.Protocol 5 | import io.github.typesafegithub.workflows.domain.VolumeMapping 6 | 7 | public fun port( 8 | mapping: Pair, 9 | protocol: Protocol = Protocol.All, 10 | ): PortMapping = PortMapping(host = mapping.first, container = mapping.second, protocol = protocol) 11 | 12 | public fun port( 13 | host: Int, 14 | protocol: Protocol = Protocol.All, 15 | ): PortMapping = PortMapping(host = host, protocol = protocol) 16 | 17 | public fun tcp(mapping: Pair): PortMapping = port(mapping, Protocol.TCP) 18 | 19 | public fun tcp(host: Int): PortMapping = port(host, Protocol.TCP) 20 | 21 | public fun udp(mapping: Pair): PortMapping = port(mapping, Protocol.UDP) 22 | 23 | public fun udp(host: Int): PortMapping = port(host, Protocol.UDP) 24 | 25 | public fun volume( 26 | mapping: Pair, 27 | isReadOnly: Boolean = false, 28 | ): VolumeMapping = VolumeMapping(source = mapping.first, target = mapping.second, isReadOnly = isReadOnly) 29 | 30 | public fun volume( 31 | target: String, 32 | isReadOnly: Boolean = false, 33 | ): VolumeMapping = VolumeMapping(target = target, isReadOnly = isReadOnly) 34 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/GithubActionsDsl.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.dsl 2 | 3 | @DslMarker 4 | internal annotation class GithubActionsDsl 5 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/HasCustomArguments.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.dsl 2 | 3 | import kotlinx.serialization.Contextual 4 | 5 | public interface HasCustomArguments { 6 | @Suppress("ktlint:standard:backing-property-naming", "VariableNaming") 7 | public val _customArguments: Map 8 | } 9 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/expressions/Contexts.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.dsl.expressions 2 | 3 | import io.github.typesafegithub.workflows.dsl.expressions.contexts.EnvContext 4 | import io.github.typesafegithub.workflows.dsl.expressions.contexts.FunctionsContext 5 | import io.github.typesafegithub.workflows.dsl.expressions.contexts.GitHubContext 6 | import io.github.typesafegithub.workflows.dsl.expressions.contexts.RunnerContext 7 | import io.github.typesafegithub.workflows.dsl.expressions.contexts.SecretsContext 8 | import io.github.typesafegithub.workflows.dsl.expressions.contexts.VarsContext 9 | 10 | /** 11 | * Root elements of GitHub expressions. 12 | * 13 | * https://docs.github.com/en/actions/learn-github-actions/expressions#about-expressions 14 | * https://docs.github.com/en/actions/learn-github-actions/contexts 15 | */ 16 | public object Contexts : FunctionsContext() { 17 | public val env: EnvContext = EnvContext 18 | public val github: GitHubContext = GitHubContext 19 | public val runner: RunnerContext = RunnerContext 20 | public val secrets: SecretsContext = SecretsContext 21 | public val vars: VarsContext = VarsContext 22 | } 23 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/expressions/Expression.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.dsl.expressions 2 | 3 | /** 4 | * Creates an expression, i.e. something evaluated by GitHub, from a string. 5 | * 6 | * https://docs.github.com/en/actions/learn-github-actions/expressions#about-expressions 7 | */ 8 | public fun expr(value: String): String = "\${{ ${value.removePrefix("$")} }}" 9 | 10 | /** 11 | * Creates an expression, i.e. something evaluated by GitHub, using type-safe API. 12 | * 13 | * https://docs.github.com/en/actions/learn-github-actions/expressions#about-expressions 14 | * https://docs.github.com/en/actions/learn-github-actions/contexts 15 | */ 16 | public fun expr(expression: Contexts.() -> String): String = with(Contexts) { expr(expression()) } 17 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/expressions/ExpressionContext.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.dsl.expressions 2 | 3 | @Suppress("ConstructorParameterNaming") 4 | public open class ExpressionContext( 5 | internal val _path: String, 6 | public val propertyToExprPath: Map = MapFromLambda { propertyName -> "$_path.$propertyName" }, 7 | ) : Map by propertyToExprPath 8 | 9 | internal class MapFromLambda( 10 | val operation: (String) -> T, 11 | ) : Map by emptyMap() { 12 | override fun containsKey(key: String) = true 13 | 14 | override fun get(key: String): T = operation(key) 15 | 16 | override fun getOrDefault( 17 | key: String, 18 | defaultValue: T, 19 | ): T = get(key) 20 | } 21 | 22 | internal class FakeList( 23 | val name: String, 24 | ) : List by emptyList() { 25 | override fun get(index: Int): String = "$name[$index]" 26 | } 27 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/expressions/contexts/FunctionsContext.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.dsl.expressions.contexts 2 | 3 | /** 4 | * GitHub offers a set of built-in functions that you can use in expressions. 5 | * Some functions cast values to a string to perform comparisons. 6 | * GitHub casts data types to a string using these conversions: 7 | * 8 | * https://docs.github.com/en/actions/learn-github-actions/expressions#functions 9 | */ 10 | @Suppress("FunctionOnlyReturningConstant", "TooManyFunctions") 11 | public open class FunctionsContext { 12 | private fun formatArgs( 13 | vararg args: String, 14 | quote: Boolean, 15 | ): String { 16 | fun String.maybeQuote(): String { 17 | val escaped = replace("'", "\\'") 18 | return if (quote) "'$escaped'" else this 19 | } 20 | return args.joinToString(prefix = "(", postfix = ")", transform = String::maybeQuote) 21 | } 22 | 23 | public fun always(): String = "always()" 24 | 25 | public fun success(): String = "success()" 26 | 27 | public fun cancelled(): String = "cancelled()" 28 | 29 | public fun failure(): String = "failure()" 30 | 31 | public fun contains( 32 | search: String, 33 | item: String, 34 | quote: Boolean = false, 35 | ): String = "contains" + formatArgs(search, item, quote = quote) 36 | 37 | public fun startsWith( 38 | searchString: String, 39 | searchValue: String, 40 | quote: Boolean = false, 41 | ): String = "startsWith" + formatArgs(searchString, searchValue, quote = quote) 42 | 43 | public fun endsWith( 44 | searchString: String, 45 | searchValue: String, 46 | quote: Boolean = false, 47 | ): String = "endsWith" + formatArgs(searchString, searchValue, quote = quote) 48 | 49 | public fun format( 50 | vararg args: String, 51 | quote: Boolean = false, 52 | ): String { 53 | require(args.isNotEmpty()) { "Expected first arg like : format('Hello {0} {1} {2}', 'Mona', 'the', 'Octocat')" } 54 | return "format" + formatArgs(*args, quote = quote) 55 | } 56 | 57 | public fun join( 58 | array: String, 59 | separator: String = ",", 60 | quote: Boolean = false, 61 | ): String = "join" + formatArgs(array, separator, quote = quote) 62 | 63 | public fun toJSON( 64 | value: String, 65 | quote: Boolean = false, 66 | ): String = "toJSON" + formatArgs(value, quote = quote) 67 | 68 | public fun fromJSON( 69 | value: String, 70 | quote: Boolean = false, 71 | ): String = "fromJSON" + formatArgs(value, quote = quote) 72 | 73 | public fun hashFiles( 74 | vararg paths: String, 75 | quote: Boolean = false, 76 | ): String = "hashFiles" + formatArgs(*paths, quote = quote) 77 | } 78 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/expressions/contexts/RunnerContext.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.dsl.expressions.contexts 2 | 3 | import io.github.typesafegithub.workflows.dsl.expressions.ExpressionContext 4 | 5 | /** 6 | * The runner context contains information about the runner that is executing the current job. 7 | * https://docs.github.com/en/actions/learn-github-actions/contexts#example-contents-of-the-runner-context 8 | */ 9 | public object RunnerContext : ExpressionContext("runner") { 10 | public val name: String by propertyToExprPath 11 | public val os: String by propertyToExprPath 12 | public val arch: String by propertyToExprPath 13 | public val temp: String by propertyToExprPath 14 | public val tool_cache: String by propertyToExprPath 15 | public val workspace: String by propertyToExprPath 16 | } 17 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/expressions/contexts/SecretsContext.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.dsl.expressions.contexts 2 | 3 | import io.github.typesafegithub.workflows.dsl.expressions.ExpressionContext 4 | 5 | /** 6 | * Encrypted secrets 7 | * 8 | * Encrypted secrets allow you to store sensitive information in your organization, 9 | * repository, or repository environments. 10 | * 11 | * https://docs.github.com/en/actions/security-guides/encrypted-secrets 12 | */ 13 | public object SecretsContext : ExpressionContext("secrets") { 14 | /*** 15 | * GITHUB_TOKEN is a secret that is automatically created for every workflow run, 16 | * and is always included in the secrets context. For more information, see "Automatic token authentication." 17 | * https://docs.github.com/en/actions/security-guides/automatic-token-authentication 18 | */ 19 | public val GITHUB_TOKEN: String by propertyToExprPath 20 | } 21 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/expressions/contexts/VarsContext.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.dsl.expressions.contexts 2 | 3 | import io.github.typesafegithub.workflows.dsl.expressions.ExpressionContext 4 | 5 | /** 6 | * Vars 7 | * 8 | * Vars allow you to store configuration variables in your organization, repository, or repository environments. 9 | * 10 | * https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#vars-context 11 | */ 12 | public object VarsContext : ExpressionContext("vars") 13 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/internal/CaseEnumSerializer.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) 2 | 3 | package io.github.typesafegithub.workflows.internal 4 | 5 | import io.github.typesafegithub.workflows.yaml.snakeCaseOf 6 | import kotlinx.serialization.ExperimentalSerializationApi 7 | import kotlinx.serialization.InternalSerializationApi 8 | import kotlinx.serialization.KSerializer 9 | import kotlinx.serialization.SerializationException 10 | import kotlinx.serialization.descriptors.SerialDescriptor 11 | import kotlinx.serialization.descriptors.SerialKind 12 | import kotlinx.serialization.descriptors.StructureKind 13 | import kotlinx.serialization.descriptors.buildSerialDescriptor 14 | import kotlinx.serialization.encoding.Decoder 15 | import kotlinx.serialization.encoding.Encoder 16 | 17 | /** 18 | * Copy/pasted from the kotlinx.serialization library 19 | * What changed is that the enum value is the PascalCase version of its snake_case 20 | */ 21 | @InternalSerializationApi 22 | internal open class CaseEnumSerializer>( 23 | serialName: String, 24 | private val values: Array, 25 | ) : KSerializer { 26 | override val descriptor: SerialDescriptor = 27 | buildSerialDescriptor(serialName, SerialKind.ENUM) { 28 | values.forEach { 29 | val fqn = "$serialName.${it.name}" 30 | val enumMemberDescriptor = buildSerialDescriptor(fqn, StructureKind.OBJECT) 31 | element(snakeCaseOf(it.name), enumMemberDescriptor) 32 | } 33 | } 34 | 35 | override fun serialize( 36 | encoder: Encoder, 37 | value: T, 38 | ) { 39 | val index = values.indexOf(value) 40 | if (index == -1) { 41 | throw SerializationException( 42 | "$value is not a valid enum ${descriptor.serialName}, " + 43 | "must be one of ${values.contentToString()}", 44 | ) 45 | } 46 | encoder.encodeEnum(descriptor, index) 47 | } 48 | 49 | override fun deserialize(decoder: Decoder): T { 50 | val index = decoder.decodeEnum(descriptor) 51 | if (index !in values.indices) { 52 | throw SerializationException( 53 | "$index is not among valid ${descriptor.serialName} enum values, " + 54 | "values size is ${values.size}", 55 | ) 56 | } 57 | return values[index] 58 | } 59 | 60 | override fun toString(): String = "kotlinx.serialization.CaseEnumSerializer<${descriptor.serialName}>" 61 | } 62 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/internal/InternalGithubActionsApi.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.internal 2 | 3 | /** 4 | * Marks declarations that are **internal** to github-workflows-kt, 5 | * which means they should not be used outside github-workflows-kt modules, 6 | * because their signatures and semantics can **(will) change** between future releases, 7 | * without any warnings and without providing any migration aids. 8 | */ 9 | @RequiresOptIn 10 | public annotation class InternalGithubActionsApi 11 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/internal/PathUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.internal 2 | 3 | import java.nio.file.Path 4 | import kotlin.io.path.absolute 5 | import kotlin.io.path.relativeTo 6 | 7 | internal fun Path.relativeToAbsolute(base: Path): Path = absolute().relativeTo(base.absolute()) 8 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/validation/Values.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.validation 2 | 3 | internal fun requireMatchesRegex( 4 | field: String, 5 | value: String, 6 | regex: Regex, 7 | url: String?, 8 | ) { 9 | require(regex.matchEntire(value) != null) { 10 | val seeUrl = if (url.isNullOrBlank()) "" else "\nSee: $url" 11 | """Invalid field ${field.replace('.', '(')}="$value") does not match regex: $regex$seeUrl""" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/Case.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.yaml 2 | 3 | import io.github.typesafegithub.workflows.internal.InternalGithubActionsApi 4 | 5 | @InternalGithubActionsApi 6 | public inline fun > T.toSnakeCase(): String = snakeCaseOf(name) 7 | 8 | @InternalGithubActionsApi 9 | public fun snakeCaseOf(name: String): String { 10 | require(name.first() !in 'a'..'z' && name.all { it in 'a'..'z' || it in 'A'..'Z' }) 11 | return pascalCaseRegex.findAll(name).joinToString(separator = "_") { it.value.lowercase() } 12 | } 13 | 14 | internal val pascalCaseRegex = Regex("[A-Z][a-z]*") 15 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ConsistencyCheckJobConfig.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.yaml 2 | 3 | import io.github.typesafegithub.workflows.domain.JobOutputs 4 | import io.github.typesafegithub.workflows.dsl.JobBuilder 5 | 6 | public val DEFAULT_CONSISTENCY_CHECK_JOB_CONFIG: ConsistencyCheckJobConfig.Configuration = 7 | ConsistencyCheckJobConfig.Configuration( 8 | condition = null, 9 | env = emptyMap(), 10 | additionalSteps = null, 11 | useLocalBindingsServerAsFallback = false, 12 | ) 13 | 14 | public sealed interface ConsistencyCheckJobConfig { 15 | public data object Disabled : ConsistencyCheckJobConfig 16 | 17 | public data class Configuration( 18 | val condition: String?, 19 | val env: Map, 20 | val additionalSteps: (JobBuilder.() -> Unit)?, 21 | /** 22 | * If the script execution step in the consistency check job fails, another attempt to execute is made with a 23 | * local bindings server running. 24 | * An assumption is made that the bindings server is under `https://bindings.krzeminski.it`. It's currently not 25 | * configurable. 26 | */ 27 | val useLocalBindingsServerAsFallback: Boolean, 28 | ) : ConsistencyCheckJobConfig 29 | } 30 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ContainerToYaml.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.yaml 2 | 3 | import io.github.typesafegithub.workflows.domain.Container 4 | import io.github.typesafegithub.workflows.domain.Credentials 5 | import io.github.typesafegithub.workflows.domain.PortMapping 6 | import io.github.typesafegithub.workflows.domain.VolumeMapping 7 | 8 | internal fun Container.toYaml(): Map = 9 | mapOfNotNullValues( 10 | "image" to image, 11 | "ports" to ports.ifEmpty { null }?.map(PortMapping::toYaml), 12 | "volumes" to volumes.ifEmpty { null }?.map(VolumeMapping::toYaml), 13 | "env" to env.ifEmpty { null }, 14 | "options" to options.ifEmpty { null }?.joinToString(" "), 15 | "credentials" to credentials?.toYaml(), 16 | ) + _customArguments 17 | 18 | private fun Credentials.toYaml(): Map = 19 | mapOf( 20 | "username" to username, 21 | "password" to password, 22 | ) 23 | 24 | internal fun VolumeMapping.toYaml(): String = 25 | buildString { 26 | if (source != null) { 27 | append(source) 28 | append(':') 29 | } 30 | 31 | append(target) 32 | 33 | if (isReadOnly) append(":ro") 34 | } 35 | 36 | internal fun PortMapping.toYaml(): String = 37 | buildString { 38 | append(host) 39 | 40 | if (container != null) { 41 | append(':') 42 | append(container) 43 | } 44 | 45 | val protocol = protocol.toYaml() 46 | if (protocol.isNotEmpty()) { 47 | append('/') 48 | append(protocol) 49 | } 50 | } 51 | 52 | private fun PortMapping.Protocol.toYaml(): String = 53 | when (this) { 54 | is PortMapping.Protocol.Custom -> value 55 | PortMapping.Protocol.All -> "" 56 | PortMapping.Protocol.TCP -> "tcp" 57 | PortMapping.Protocol.UDP -> "udp" 58 | } 59 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/ContextMapping.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.yaml 2 | 3 | import io.github.typesafegithub.workflows.domain.contexts.Contexts 4 | import io.github.typesafegithub.workflows.domain.contexts.GithubContext 5 | import kotlinx.serialization.json.Json 6 | 7 | internal fun loadContextsFromEnvVars(getenv: (String) -> String?): Contexts { 8 | fun getEnvVarOrFail(varName: String): String = getenv(varName) ?: error("$varName should be set!") 9 | 10 | val githubContextRaw = getEnvVarOrFail("GHWKT_GITHUB_CONTEXT_JSON") 11 | val githubContext = json.decodeFromString(githubContextRaw) 12 | return Contexts( 13 | github = githubContext, 14 | ) 15 | } 16 | 17 | private val json = Json { ignoreUnknownKeys = true } 18 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/Preamble.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.yaml 2 | 3 | public sealed class Preamble( 4 | public val content: String, 5 | ) { 6 | public class Just( 7 | content: String, 8 | ) : Preamble(content) 9 | 10 | public class WithOriginalBefore( 11 | content: String, 12 | ) : Preamble(content) 13 | 14 | public class WithOriginalAfter( 15 | content: String, 16 | ) : Preamble(content) 17 | } 18 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/StepsToYaml.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.yaml 2 | 3 | import io.github.typesafegithub.workflows.domain.ActionStep 4 | import io.github.typesafegithub.workflows.domain.CommandStep 5 | import io.github.typesafegithub.workflows.domain.KotlinLogicStep 6 | import io.github.typesafegithub.workflows.domain.Shell 7 | import io.github.typesafegithub.workflows.domain.Shell.Bash 8 | import io.github.typesafegithub.workflows.domain.Shell.Cmd 9 | import io.github.typesafegithub.workflows.domain.Shell.Custom 10 | import io.github.typesafegithub.workflows.domain.Shell.PowerShell 11 | import io.github.typesafegithub.workflows.domain.Shell.Pwsh 12 | import io.github.typesafegithub.workflows.domain.Shell.Python 13 | import io.github.typesafegithub.workflows.domain.Shell.Sh 14 | import io.github.typesafegithub.workflows.domain.Step 15 | 16 | internal fun List>.stepsToYaml(): List> = this.map { it.toYaml() } 17 | 18 | private fun Step<*>.toYaml() = 19 | when (this) { 20 | is ActionStep<*> -> toYaml() 21 | is CommandStep -> toYaml() 22 | is KotlinLogicStep -> toYaml() 23 | } 24 | 25 | private fun ActionStep<*>.toYaml(): Map = 26 | mapOfNotNullValues( 27 | "id" to id, 28 | "name" to name, 29 | "continue-on-error" to continueOnError, 30 | "timeout-minutes" to timeoutMinutes, 31 | "uses" to action.usesString, 32 | "with" to action.toYamlArguments().ifEmpty { null }, 33 | "env" to env.ifEmpty { null }, 34 | "if" to condition, 35 | ) + _customArguments 36 | 37 | private fun CommandStep.toYaml(): Map = 38 | mapOfNotNullValues( 39 | "id" to id, 40 | "name" to name, 41 | "env" to env.ifEmpty { null }, 42 | "continue-on-error" to continueOnError, 43 | "timeout-minutes" to timeoutMinutes, 44 | "shell" to shell?.toYaml(), 45 | "working-directory" to workingDirectory, 46 | "run" to command, 47 | "if" to condition, 48 | ) + _customArguments 49 | 50 | private fun KotlinLogicStep.toYaml(): Map = 51 | mapOfNotNullValues( 52 | "id" to id, 53 | "name" to name, 54 | "env" to env.ifEmpty { null }, 55 | "continue-on-error" to continueOnError, 56 | "timeout-minutes" to timeoutMinutes, 57 | "shell" to shell?.toYaml(), 58 | "working-directory" to workingDirectory, 59 | "run" to command, 60 | "if" to condition, 61 | ) + _customArguments 62 | 63 | private fun Shell.toYaml() = 64 | when (this) { 65 | Bash -> "bash" 66 | Cmd -> "cmd" 67 | Pwsh -> "pwsh" 68 | PowerShell -> "powershell" 69 | Python -> "python" 70 | Sh -> "sh" 71 | is Custom -> this.value 72 | } 73 | -------------------------------------------------------------------------------- /github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/yaml/Utils.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.yaml 2 | 3 | @Suppress("UNCHECKED_CAST", "SpreadOperator") 4 | internal fun mapOfNotNullValues(vararg pairs: Pair): Map = 5 | mapOf(*(pairs.filter { it.second != null } as List>).toTypedArray()) 6 | -------------------------------------------------------------------------------- /github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/actions/CustomActionTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.actions 2 | 3 | import io.github.typesafegithub.workflows.domain.actions.CustomAction 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.matchers.shouldBe 6 | 7 | class CustomActionTest : 8 | FunSpec({ 9 | 10 | test("custom action") { 11 | // given 12 | val customAction = 13 | CustomAction( 14 | actionOwner = "xu-cheng", 15 | actionName = "latex-action", 16 | actionVersion = "v2", 17 | inputs = 18 | mapOf( 19 | "root_file" to "report.tex", 20 | "compiler" to "latexmk", 21 | ), 22 | ) 23 | 24 | // given 25 | val outputs = customAction.buildOutputObject("someStepId") 26 | 27 | // when & then 28 | outputs["custom-output"] shouldBe "steps.someStepId.outputs.custom-output" 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/docsnippets/CompensatingLibrarysMissingFeaturesSnippets.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") // Use deprecated action versions, to not have to update them. 2 | 3 | package io.github.typesafegithub.workflows.docsnippets 4 | 5 | import io.github.typesafegithub.workflows.actions.actions.UploadArtifact 6 | import io.github.typesafegithub.workflows.domain.RunnerType.UbuntuLatest 7 | import io.github.typesafegithub.workflows.domain.triggers.Push 8 | import io.github.typesafegithub.workflows.dsl.expressions.expr 9 | import io.github.typesafegithub.workflows.dsl.workflow 10 | import io.kotest.core.spec.style.FunSpec 11 | import io.kotest.engine.spec.tempdir 12 | 13 | class CompensatingLibrarysMissingFeaturesSnippets : 14 | FunSpec({ 15 | val gitRootDir = 16 | tempdir() 17 | .also { 18 | it.resolve(".git").mkdirs() 19 | }.toPath() 20 | val sourceTempFile = gitRootDir.resolve(".github/workflows/some_workflow.main.kts").toFile() 21 | 22 | test("customArguments") { 23 | // --8<-- [start:custom-arguments-1] 24 | workflow( 25 | // --8<-- [end:custom-arguments-1] 26 | name = "customArguments", 27 | on = listOf(Push()), 28 | sourceFile = sourceTempFile, 29 | // --8<-- [start:custom-arguments-2] 30 | // ... 31 | _customArguments = 32 | mapOf( 33 | "dry-run" to true, 34 | "some-string-value" to "foobar", 35 | "written-by" to listOf("Alice", "Bob"), 36 | "concurrency" to 37 | mapOf( 38 | "group" to expr("github.ref"), 39 | "cancel-in-progress" to "true", 40 | ), 41 | ), 42 | ) 43 | // --8<-- [end:custom-arguments-2] 44 | { 45 | job(id = "test_job", runsOn = UbuntuLatest) { 46 | run(command = "echo 'Hello world!'") 47 | } 48 | } 49 | } 50 | 51 | test("customInputs") { 52 | // --8<-- [start:custom-inputs-1] 53 | UploadArtifact( 54 | // --8<-- [end:custom-inputs-1] 55 | path = emptyList(), 56 | // --8<-- [start:custom-inputs-2] 57 | // ... 58 | _customInputs = 59 | mapOf( 60 | "path" to "override-path-value", 61 | "answer" to "42", 62 | ), 63 | ) 64 | // --8<-- [end:custom-inputs-2] 65 | } 66 | 67 | test("customVersion") { 68 | // --8<-- [start:custom-version-1] 69 | UploadArtifact( 70 | // --8<-- [end:custom-version-1] 71 | path = emptyList(), 72 | // --8<-- [start:custom-version-2] 73 | // ... 74 | _customVersion = "v4", 75 | ) 76 | // --8<-- [end:custom-version-2] 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/docsnippets/GettingStartedSnippets.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("ktlint:standard:import-ordering") 2 | 3 | package io.github.typesafegithub.workflows.docsnippets 4 | 5 | import io.kotest.core.spec.style.FunSpec 6 | // --8<-- [start:getting-started-2] 7 | import io.github.typesafegithub.workflows.actions.actions.Checkout 8 | import io.github.typesafegithub.workflows.domain.RunnerType.UbuntuLatest 9 | import io.github.typesafegithub.workflows.domain.triggers.Push 10 | import io.github.typesafegithub.workflows.dsl.workflow 11 | // --8<-- [end:getting-started-2] 12 | import java.io.File 13 | 14 | class GettingStartedSnippets : 15 | FunSpec({ 16 | test("gettingStarted") { 17 | /* 18 | // --8<-- [start:getting-started-1] 19 | #!/usr/bin/env kotlin 20 | 21 | @file:Repository("https://repo.maven.apache.org/maven2/") 22 | @file:DependsOn("io.github.typesafegithub:github-workflows-kt:3.4.1-SNAPSHOT") 23 | @file:Repository("https://bindings.krzeminski.it") 24 | @file:DependsOn("actions:checkout:v4") 25 | 26 | // --8<-- [end:getting-started-1] 27 | */ 28 | @Suppress("VariableNaming", "ktlint:standard:backing-property-naming") 29 | val __FILE__ = File("") 30 | // --8<-- [start:getting-started-3] 31 | 32 | workflow( 33 | name = "Test workflow", 34 | on = listOf(Push()), 35 | sourceFile = __FILE__, 36 | ) { 37 | job(id = "test_job", runsOn = UbuntuLatest) { 38 | uses(name = "Check out", action = Checkout()) 39 | run(name = "Print greeting", command = "echo 'Hello world!'") 40 | } 41 | } 42 | // --8<-- [end:getting-started-3] 43 | } 44 | }) 45 | -------------------------------------------------------------------------------- /github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/domain/RunnerTypeTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain 2 | 3 | import io.kotest.assertions.throwables.shouldThrow 4 | import io.kotest.core.spec.style.FunSpec 5 | 6 | class RunnerTypeTest : 7 | FunSpec({ 8 | context("Labelled") { 9 | test("should throw on invalid arguments") { 10 | shouldThrow { 11 | RunnerType.Labelled() 12 | } 13 | shouldThrow { 14 | RunnerType.Labelled(emptySet()) 15 | } 16 | } 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/domain/StepTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain 2 | 3 | import io.github.typesafegithub.workflows.actions.actions.Checkout 4 | import io.github.typesafegithub.workflows.domain.AbstractResult.Status 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.kotest.matchers.shouldBe 7 | 8 | class StepTest : 9 | FunSpec({ 10 | test("step.outcome") { 11 | val step0: Step<*> = CommandStep(id = "step-0", command = "ls") 12 | step0.outcome.toString() shouldBe "steps.step-0.outcome" 13 | step0.outcome eq Status.Failure shouldBe "steps.step-0.outcome == 'failure'" 14 | step0.outcome eq Status.Cancelled shouldBe "steps.step-0.outcome == 'cancelled'" 15 | step0.outcome eq Status.Skipped shouldBe "steps.step-0.outcome == 'skipped'" 16 | step0.outcome eq Status.Success shouldBe "steps.step-0.outcome == 'success'" 17 | } 18 | test("step.conclusion") { 19 | val someStep: Step<*> = 20 | ActionStep( 21 | id = "whatever", 22 | action = Checkout(), 23 | ) 24 | someStep.conclusion.toString() shouldBe "steps.whatever.conclusion" 25 | someStep.conclusion eq Status.Failure shouldBe "steps.whatever.conclusion == 'failure'" 26 | someStep.conclusion eq Status.Cancelled shouldBe "steps.whatever.conclusion == 'cancelled'" 27 | someStep.conclusion eq Status.Skipped shouldBe "steps.whatever.conclusion == 'skipped'" 28 | someStep.conclusion eq Status.Success shouldBe "steps.whatever.conclusion == 'success'" 29 | } 30 | test("step.outputs") { 31 | val step0: Step<*> = CommandStep(id = "step-0", command = "ls") 32 | step0.outputs["foo"] shouldBe "steps.step-0.outputs.foo" 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/domain/triggers/PullRequestTargetTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import io.kotest.assertions.throwables.shouldThrow 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.matchers.shouldBe 6 | 7 | class PullRequestTargetTest : 8 | FunSpec({ 9 | context("validation errors") { 10 | test("both 'branches' and 'branchesIgnore' defined") { 11 | val exception = 12 | shouldThrow { 13 | PullRequestTarget( 14 | branches = listOf("branch1"), 15 | branchesIgnore = listOf("branch2"), 16 | ) 17 | } 18 | exception.message shouldBe "Cannot define both 'branches' and 'branchesIgnore'!" 19 | } 20 | 21 | test("both 'paths' and 'pathsIgnore' defined") { 22 | val exception = 23 | shouldThrow { 24 | PullRequestTarget( 25 | paths = listOf("path1"), 26 | pathsIgnore = listOf("path2"), 27 | ) 28 | } 29 | exception.message shouldBe "Cannot define both 'paths' and 'pathsIgnore'!" 30 | } 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/domain/triggers/PullRequestTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import io.kotest.assertions.throwables.shouldThrow 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.matchers.shouldBe 6 | 7 | class PullRequestTest : 8 | FunSpec({ 9 | context("validation errors") { 10 | test("both 'branches' and 'branchesIgnore' defined") { 11 | val exception = 12 | shouldThrow { 13 | PullRequest( 14 | branches = listOf("branch1"), 15 | branchesIgnore = listOf("branch2"), 16 | ) 17 | } 18 | exception.message shouldBe "Cannot define both 'branches' and 'branchesIgnore'!" 19 | } 20 | 21 | test("both 'paths' and 'pathsIgnore' defined") { 22 | val exception = 23 | shouldThrow { 24 | PullRequest( 25 | paths = listOf("path1"), 26 | pathsIgnore = listOf("path2"), 27 | ) 28 | } 29 | exception.message shouldBe "Cannot define both 'paths' and 'pathsIgnore'!" 30 | } 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/domain/triggers/PushTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.domain.triggers 2 | 3 | import io.kotest.assertions.throwables.shouldThrow 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.matchers.shouldBe 6 | 7 | class PushTest : 8 | FunSpec({ 9 | context("validation errors") { 10 | test("both 'branches' and 'branchesIgnore' defined") { 11 | val exception = 12 | shouldThrow { 13 | Push( 14 | branches = listOf("branch1"), 15 | branchesIgnore = listOf("branch2"), 16 | ) 17 | } 18 | exception.message shouldBe "Cannot define both 'branches' and 'branchesIgnore'!" 19 | } 20 | 21 | test("both 'tags' and 'tagsIgnore' defined") { 22 | val exception = 23 | shouldThrow { 24 | Push( 25 | tags = listOf("tag2"), 26 | tagsIgnore = listOf("tag2"), 27 | ) 28 | } 29 | exception.message shouldBe "Cannot define both 'tags' and 'tagsIgnore'!" 30 | } 31 | 32 | test("both 'paths' and 'pathsIgnore' defined") { 33 | val exception = 34 | shouldThrow { 35 | Push( 36 | paths = listOf("path1"), 37 | pathsIgnore = listOf("path2"), 38 | ) 39 | } 40 | exception.message shouldBe "Cannot define both 'paths' and 'pathsIgnore'!" 41 | } 42 | } 43 | }) 44 | -------------------------------------------------------------------------------- /github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/dsl/expressions/PayloadTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.dsl.expressions 2 | 3 | import io.github.typesafegithub.workflows.dsl.expressions.contexts.RunnerContext 4 | import io.kotest.assertions.fail 5 | import io.kotest.core.spec.style.FunSpec 6 | import kotlinx.serialization.json.Json 7 | import kotlinx.serialization.json.jsonObject 8 | import java.io.File 9 | import kotlin.reflect.KClass 10 | import kotlin.reflect.full.declaredMemberProperties 11 | 12 | class PayloadTest : 13 | FunSpec({ 14 | val payloads = File("src/test/resources/payloads") 15 | 16 | fun KClass<*>.properties(): List = declaredMemberProperties.map { it.name }.sorted() 17 | 18 | test("Runner context") { 19 | val context = RunnerContext::class 20 | val file = payloads.resolve("runner.json") 21 | val jsonObject = Json.parseToJsonElement(file.readText()).jsonObject 22 | 23 | context.properties() shouldMatch jsonObject.keys.sorted() 24 | } 25 | }) 26 | 27 | infix fun List.shouldMatch(other: List) { 28 | if (this.toSet() != other.toSet()) { 29 | fail( 30 | """ 31 | |The lists don't match 32 | |Missing: ${(other - toSet()).distinct().sorted()} 33 | |Unexpected: ${(this - other.toSet()).distinct().sorted()} 34 | """.trimMargin(), 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/yaml/CaseTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.yaml 2 | 3 | import io.github.typesafegithub.workflows.domain.triggers.PullRequest 4 | import io.github.typesafegithub.workflows.domain.triggers.PullRequest.Type 5 | import io.github.typesafegithub.workflows.domain.triggers.PullRequestTarget 6 | import io.kotest.assertions.throwables.shouldThrowAny 7 | import io.kotest.core.spec.style.DescribeSpec 8 | import io.kotest.inspectors.forAll 9 | import io.kotest.matchers.shouldBe 10 | 11 | class CaseTest : 12 | DescribeSpec({ 13 | it("transforms to pascal case") { 14 | listOf( 15 | Type.Assigned to "assigned", 16 | Type.AutoMergeDisabled to "auto_merge_disabled", 17 | Type.ReviewRequested to "review_requested", 18 | ).forAll { (type, expected) -> 19 | type.toSnakeCase() shouldBe expected 20 | } 21 | } 22 | 23 | it("should fail early if the enum is not in pascal case") { 24 | MyEnum.values().forAll { enum -> 25 | shouldThrowAny { 26 | enum.toSnakeCase() 27 | } 28 | } 29 | } 30 | 31 | it("all enums should be in pascal case") { 32 | PullRequestTarget.Type.values().forAll { it.toSnakeCase() } 33 | PullRequest.Type.values().forAll { it.toSnakeCase() } 34 | } 35 | }) 36 | 37 | private enum class MyEnum { 38 | @Suppress("ktlint:standard:enum-entry-name-case") 39 | In_valid, 40 | } 41 | -------------------------------------------------------------------------------- /github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/yaml/ContextMappingTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.yaml 2 | 3 | import io.github.typesafegithub.workflows.domain.contexts.Contexts 4 | import io.github.typesafegithub.workflows.domain.contexts.GithubContext 5 | import io.github.typesafegithub.workflows.domain.contexts.GithubContextEvent 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.matchers.shouldBe 8 | 9 | class ContextMappingTest : 10 | FunSpec({ 11 | test("successfully load all supported contexts with all supported fields") { 12 | // Given 13 | val testGithubContextJson = javaClass.getResource("/contexts/github-all-fields.json")!!.readText() 14 | 15 | // When 16 | val contexts = 17 | loadContextsFromEnvVars( 18 | getenv = mapOf( 19 | "GHWKT_GITHUB_CONTEXT_JSON" to testGithubContextJson, 20 | )::get, 21 | ) 22 | 23 | // Then 24 | contexts shouldBe 25 | Contexts( 26 | github = 27 | GithubContext( 28 | repository = "some-owner/some-repo", 29 | sha = "db76dd0f1149901e1cdf60ec98d568b32fa7eb71", 30 | ref = "refs/heads/main", 31 | base_ref = "refs/heads/develop", 32 | event = 33 | GithubContextEvent( 34 | after = "1383af4847629428f1675f5c2e81e67cc3a4efb0", 35 | ), 36 | event_name = "push", 37 | ), 38 | ) 39 | } 40 | 41 | test("successfully load all supported contexts with only required fields") { 42 | // Given 43 | val testGithubContextJson = javaClass.getResource("/contexts/github-required-fields.json")!!.readText() 44 | 45 | // When 46 | val contexts = 47 | loadContextsFromEnvVars( 48 | getenv = mapOf( 49 | "GHWKT_GITHUB_CONTEXT_JSON" to testGithubContextJson, 50 | )::get, 51 | ) 52 | 53 | // Then 54 | contexts shouldBe 55 | Contexts( 56 | github = 57 | GithubContext( 58 | repository = "some-owner/some-repo", 59 | sha = "db76dd0f1149901e1cdf60ec98d568b32fa7eb71", 60 | event = GithubContextEvent(), 61 | event_name = "push", 62 | ), 63 | ) 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /github-workflows-kt/src/test/resources/contexts/github-all-fields.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": "some-owner/some-repo", 3 | "sha": "db76dd0f1149901e1cdf60ec98d568b32fa7eb71", 4 | "ref": "refs/heads/main", 5 | "base_ref": "refs/heads/develop", 6 | "event": { 7 | "after": "1383af4847629428f1675f5c2e81e67cc3a4efb0" 8 | }, 9 | "event_name": "push" 10 | } -------------------------------------------------------------------------------- /github-workflows-kt/src/test/resources/contexts/github-required-fields.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": "some-owner/some-repo", 3 | "sha": "db76dd0f1149901e1cdf60ec98d568b32fa7eb71", 4 | "event": {}, 5 | "event_name": "push" 6 | } -------------------------------------------------------------------------------- /github-workflows-kt/src/test/resources/payloads/runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "os": "Linux", 3 | "arch": "X64", 4 | "name": "GitHub Actions 2", 5 | "tool_cache": "/opt/hostedtoolcache", 6 | "temp": "/home/runner/work/_temp", 7 | "workspace": "/home/runner/work/github-actions-kotlin-dsl" 8 | } 9 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 2 | org.gradle.caching=true 3 | org.gradle.configuration-cache=true 4 | 5 | org.gradle.java.installations.auto-detect=true 6 | org.gradle.java.installations.auto-download=false 7 | 8 | kotlin.code.style=official 9 | kotlin.incremental.useClasspathSnapshot=true 10 | 11 | # So that KSP doesn't generate files in "test" sources root 12 | ksp.allow.all.target.configuration=false 13 | 14 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 15 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typesafegithub/github-workflows-kt/45931670d158f31b3330fe1e6c594be8cadd9a0f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /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 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /jit-binding-server/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import io.ktor.plugin.features.* 2 | 3 | plugins { 4 | buildsrc.convention.`kotlin-jvm-server` 5 | application 6 | id("io.ktor.plugin") version "3.1.3" 7 | id("io.gitlab.arturbosch.detekt") 8 | } 9 | 10 | dependencies { 11 | implementation(platform("io.ktor:ktor-bom:3.1.3")) 12 | implementation("io.ktor:ktor-server-core") 13 | implementation("io.ktor:ktor-server-netty") 14 | implementation("io.ktor:ktor-server-call-logging") 15 | implementation("io.ktor:ktor-server-call-id") 16 | implementation("io.ktor:ktor-server-metrics-micrometer") 17 | implementation("io.micrometer:micrometer-registry-prometheus:1.15.0") 18 | 19 | implementation("com.sksamuel.aedile:aedile-core:2.1.2") 20 | implementation("io.github.oshai:kotlin-logging:7.0.7") 21 | implementation(platform("org.apache.logging.log4j:log4j-bom:2.24.3")) 22 | implementation("org.apache.logging.log4j:log4j-jul") 23 | runtimeOnly("org.apache.logging.log4j:log4j-core") 24 | runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl") 25 | runtimeOnly("org.apache.logging.log4j:log4j-jpl") 26 | 27 | implementation(projects.mavenBindingBuilder) 28 | implementation(projects.sharedInternal) 29 | 30 | testImplementation("io.ktor:ktor-server-test-host") 31 | testImplementation("io.mockk:mockk:1.14.2") 32 | } 33 | 34 | application { 35 | mainClass.set("io.github.typesafegithub.workflows.jitbindingserver.MainKt") 36 | } 37 | 38 | val dockerAppName = "github-workflows-kt-jit-binding-server" 39 | 40 | ktor { 41 | docker { 42 | localImageName.set(dockerAppName) 43 | 44 | externalRegistry.set( 45 | DockerImageRegistry.dockerHub( 46 | appName = provider { dockerAppName }, 47 | username = providers.environmentVariable("DOCKERHUB_USERNAME"), 48 | password = providers.environmentVariable("DOCKERHUB_PASSWORD"), 49 | ), 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ActionCoords.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.jitbindingserver 2 | 3 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords 4 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion 5 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL 6 | import io.ktor.http.Parameters 7 | 8 | fun Parameters.extractActionCoords(extractVersion: Boolean): ActionCoords { 9 | val owner = this["owner"]!! 10 | val nameAndPathAndSignificantVersionParts = this["name"]!!.split("___", limit = 2) 11 | val nameAndPath = nameAndPathAndSignificantVersionParts.first() 12 | val significantVersion = 13 | nameAndPathAndSignificantVersionParts 14 | .drop(1) 15 | .takeIf { it.isNotEmpty() } 16 | ?.single() 17 | ?.let { significantVersionString -> 18 | SignificantVersion 19 | .entries 20 | .find { "$it" == significantVersionString } 21 | } ?: FULL 22 | val nameAndPathParts = nameAndPath.split("__") 23 | val name = nameAndPathParts.first() 24 | val path = 25 | nameAndPathParts 26 | .drop(1) 27 | .joinToString("/") 28 | .takeUnless { it.isBlank() } 29 | val version = if (extractVersion) this["version"]!! else "irrelevant" 30 | 31 | return ActionCoords(owner, name, version, significantVersion, path) 32 | } 33 | -------------------------------------------------------------------------------- /jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/InternalRoutes.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.jitbindingserver 2 | 3 | import io.ktor.server.response.respondText 4 | import io.ktor.server.routing.Routing 5 | import io.ktor.server.routing.get 6 | import io.micrometer.prometheusmetrics.PrometheusMeterRegistry 7 | 8 | fun Routing.internalRoutes(prometheusRegistry: PrometheusMeterRegistry) { 9 | get("/metrics") { 10 | call.respondText(text = prometheusRegistry.scrape()) 11 | } 12 | 13 | get("/status") { 14 | call.respondText(text = "OK") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/MetadataRoutes.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.jitbindingserver 2 | 3 | import com.sksamuel.aedile.core.LoadingCache 4 | import io.github.oshai.kotlinlogging.KotlinLogging.logger 5 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords 6 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.prettyPrintWithoutVersion 7 | import io.ktor.server.response.respondText 8 | import io.ktor.server.routing.Route 9 | import io.ktor.server.routing.Routing 10 | import io.ktor.server.routing.get 11 | import io.ktor.server.routing.route 12 | import io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics 13 | import io.micrometer.prometheusmetrics.PrometheusMeterRegistry 14 | 15 | private val logger = logger { } 16 | 17 | typealias CachedMetadataArtifact = Map 18 | 19 | fun Routing.metadataRoutes( 20 | metadataCache: LoadingCache, 21 | prometheusRegistry: PrometheusMeterRegistry? = null, 22 | ) { 23 | prometheusRegistry?.let { 24 | CaffeineCacheMetrics.monitor(it, metadataCache.underlying(), "metadata_cache") 25 | } 26 | 27 | route("{owner}/{name}/{file}") { 28 | metadata(metadataCache) 29 | } 30 | 31 | route("/refresh/{owner}/{name}/{file}") { 32 | metadata(metadataCache, refresh = true) 33 | } 34 | } 35 | 36 | private fun Route.metadata( 37 | metadataCache: LoadingCache, 38 | refresh: Boolean = false, 39 | ) { 40 | get { 41 | val actionCoords = call.parameters.extractActionCoords(extractVersion = false) 42 | 43 | logger.info { "➡️ Requesting metadata for ${actionCoords.prettyPrintWithoutVersion}" } 44 | 45 | if (refresh) { 46 | metadataCache.invalidate(actionCoords) 47 | } 48 | val metadataArtifacts = metadataCache.get(actionCoords) 49 | 50 | if (refresh && !deliverOnRefreshRoute) return@get call.respondText(text = "OK") 51 | 52 | val file = call.parameters["file"] ?: return@get call.respondNotFound() 53 | 54 | if (file in metadataArtifacts) { 55 | when (val artifact = metadataArtifacts[file]) { 56 | is String -> call.respondText(text = artifact) 57 | else -> call.respondNotFound() 58 | } 59 | } else { 60 | call.respondNotFound() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Plugins.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.jitbindingserver 2 | 3 | import io.ktor.http.HttpHeaders 4 | import io.ktor.server.application.Application 5 | import io.ktor.server.application.install 6 | import io.ktor.server.metrics.micrometer.MicrometerMetrics 7 | import io.ktor.server.plugins.callid.CallId 8 | import io.ktor.server.plugins.callid.callIdMdc 9 | import io.ktor.server.plugins.callid.generate 10 | import io.ktor.server.plugins.calllogging.CallLogging 11 | import io.micrometer.prometheusmetrics.PrometheusMeterRegistry 12 | 13 | fun Application.installPlugins(prometheusRegistry: PrometheusMeterRegistry) { 14 | install(CallId) { 15 | generate(length = 15, dictionary = "abcdefghijklmnopqrstuvwxyz0123456789") 16 | replyToHeader(HttpHeaders.XRequestId) 17 | } 18 | 19 | install(CallLogging) { 20 | callIdMdc("request-id") 21 | } 22 | 23 | install(MicrometerMetrics) { 24 | registry = prometheusRegistry 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /jit-binding-server/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%-35.35t> <%x> <%X> <%50.50c> %m}{TRACE = magenta}%n]]> 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <%-35.35t> <%x> <%X> <%50.50c> %m}{TRACE = magenta}%n]]> 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /maven-binding-builder/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | buildsrc.convention.`kotlin-jvm` 3 | } 4 | 5 | dependencies { 6 | implementation("org.jetbrains.kotlin:kotlin-compiler") 7 | api("io.arrow-kt:arrow-core:2.1.2") 8 | api(projects.actionBindingGenerator) 9 | implementation(projects.sharedInternal) 10 | implementation("io.github.oshai:kotlin-logging:7.0.7") 11 | 12 | runtimeOnly(projects.githubWorkflowsKt) 13 | } 14 | -------------------------------------------------------------------------------- /maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/ActionCoordsUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.mavenbinding 2 | 3 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords 4 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL 5 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.subName 6 | 7 | internal val ActionCoords.mavenName: String get() = "$name${subName.replace("/", "__")}${ 8 | significantVersion.takeUnless { it == FULL }?.let { "___$it" } ?: "" 9 | }" 10 | -------------------------------------------------------------------------------- /maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/Checksums.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.mavenbinding 2 | 3 | import java.security.MessageDigest 4 | 5 | internal fun ByteArray.md5Checksum() = checksum("MD5") 6 | 7 | internal fun String.md5Checksum() = checksum("MD5") 8 | 9 | internal fun ByteArray.sha1Checksum() = checksum("SHA-1") 10 | 11 | internal fun String.sha1Checksum() = checksum("SHA-1") 12 | 13 | internal fun ByteArray.sha256Checksum() = checksum("SHA-256") 14 | 15 | internal fun String.sha256Checksum() = checksum("SHA-256") 16 | 17 | internal fun ByteArray.sha512Checksum() = checksum("SHA-512") 18 | 19 | internal fun String.sha512Checksum() = checksum("SHA-512") 20 | 21 | private fun ByteArray.checksum(algorithm: String): String { 22 | val digest = MessageDigest.getInstance(algorithm) 23 | val hashBytes = digest.digest(this) 24 | return hashBytes.joinToString("") { "%02x".format(it) } 25 | } 26 | 27 | private fun String.checksum(algorithm: String) = toByteArray(charset = Charsets.UTF_8).checksum(algorithm) 28 | -------------------------------------------------------------------------------- /maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.mavenbinding 2 | 3 | import arrow.core.Either 4 | import arrow.core.getOrElse 5 | import io.github.oshai.kotlinlogging.KotlinLogging.logger 6 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords 7 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL 8 | import io.github.typesafegithub.workflows.shared.internal.fetchAvailableVersions 9 | import io.github.typesafegithub.workflows.shared.internal.model.Version 10 | import java.time.format.DateTimeFormatter 11 | 12 | private val logger = logger { } 13 | 14 | internal suspend fun ActionCoords.buildMavenMetadataFile( 15 | githubAuthToken: String, 16 | fetchAvailableVersions: suspend ( 17 | owner: String, 18 | name: String, 19 | githubAuthToken: String?, 20 | ) -> Either> = ::fetchAvailableVersions, 21 | prefetchBindingArtifacts: (Collection) -> Unit = {}, 22 | ): String? { 23 | val availableVersions = 24 | fetchAvailableVersions(owner, name, githubAuthToken) 25 | .getOrElse { 26 | logger.error { it } 27 | emptyList() 28 | }.filter { it.isMajorVersion() || (significantVersion < FULL) } 29 | prefetchBindingArtifacts(availableVersions.map { copy(version = "$it") }) 30 | val newest = availableVersions.maxOrNull() ?: return null 31 | val lastUpdated = 32 | DateTimeFormatter 33 | .ofPattern("yyyyMMddHHmmss") 34 | .format(newest.getReleaseDate()) 35 | return """ 36 | 37 | 38 | $owner 39 | $mavenName 40 | 41 | $newest 42 | $newest 43 | 44 | ${availableVersions.joinToString(separator = "\n") { 45 | " $it" 46 | }} 47 | 48 | $lastUpdated 49 | 50 | 51 | """.trimIndent() 52 | } 53 | -------------------------------------------------------------------------------- /maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/PackageArtifactsBuilding.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.mavenbinding 2 | 3 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords 4 | 5 | suspend fun buildPackageArtifacts( 6 | actionCoords: ActionCoords, 7 | githubAuthToken: String, 8 | prefetchBindingArtifacts: (Collection) -> Unit, 9 | ): Map { 10 | val mavenMetadata = 11 | actionCoords.buildMavenMetadataFile( 12 | githubAuthToken = githubAuthToken, 13 | prefetchBindingArtifacts = prefetchBindingArtifacts, 14 | ) ?: return emptyMap() 15 | return mapOf( 16 | "maven-metadata.xml" to mavenMetadata, 17 | "maven-metadata.xml.md5" to mavenMetadata.md5Checksum(), 18 | "maven-metadata.xml.sha1" to mavenMetadata.sha1Checksum(), 19 | "maven-metadata.xml.sha256" to mavenMetadata.sha256Checksum(), 20 | "maven-metadata.xml.sha512" to mavenMetadata.sha512Checksum(), 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/PomBuilding.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.mavenbinding 2 | 3 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords 4 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.fullName 5 | import io.github.typesafegithub.workflows.actionbindinggenerator.domain.prettyPrint 6 | 7 | internal const val LATEST_RELASED_LIBRARY_VERSION = "3.4.0" 8 | 9 | internal fun ActionCoords.buildPomFile() = 10 | """ 11 | 12 | 13 | 4.0.0 14 | $owner 15 | $mavenName 16 | $version 17 | $fullName 18 | Auto-generated binding for $prettyPrint. 19 | https://github.com/$owner/$name 20 | 21 | scm:git:git://github.com/$owner/$name.git/ 22 | scm:git:ssh://github.com:$owner/$name.git 23 | https://github.com/$owner/$name.git 24 | 25 | 26 | 27 | io.github.typesafegithub 28 | github-workflows-kt 29 | $LATEST_RELASED_LIBRARY_VERSION 30 | compile 31 | 32 | 33 | 34 | """.trimIndent() 35 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # pip install -r docs/requirements.txt 2 | # mkdocs serve 3 | 4 | site_name: github-workflows-kt 5 | site_author: Piotr Krzemiński 6 | repo_url: https://github.com/typesafegithub/github-workflows-kt/ 7 | edit_uri: https://github.com/typesafegithub/github-workflows-kt/tree/main/docs 8 | 9 | theme: 10 | logo: Logo.svg 11 | name: material 12 | features: 13 | - navigation.expand 14 | palette: 15 | - scheme: default 16 | media: "(prefers-color-scheme: light)" 17 | toggle: 18 | icon: material/toggle-switch-off-outline 19 | name: Switch to dark mode 20 | - scheme: slate 21 | media: "(prefers-color-scheme: dark)" 22 | toggle: 23 | icon: material/toggle-switch 24 | name: Switch to light mode 25 | 26 | markdown_extensions: 27 | - pymdownx.details 28 | - pymdownx.magiclink 29 | - pymdownx.inlinehilite 30 | - pymdownx.superfences 31 | - pymdownx.snippets: 32 | base_path: 33 | - github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/docsnippets 34 | check_paths: True 35 | dedent_subsections: True 36 | - admonition 37 | 38 | plugins: 39 | - search 40 | - mkdocs-video 41 | 42 | nav: 43 | - Introduction: 'index.md' 44 | - User guide: 45 | - Getting started: 'user-guide/getting_started.md' 46 | - Using actions: 'user-guide/using-actions.md' 47 | - Type-safe expressions: 'user-guide/type-safe-expressions.md' 48 | - Job outputs: 'user-guide/job-outputs.md' 49 | - Migrating to Maven-based bindings: 'user-guide/migrating-to-Maven-based-bindings.md' 50 | - Compensating library's missing features: 'user-guide/compensating-librarys-missing-features.md' 51 | - Nightly builds: 'user-guide/nightly-builds.md' 52 | - Feature coverage: 'feature-coverage.md' 53 | - FAQ: 'faq.md' 54 | - API docs: 'https://typesafegithub.github.io/github-workflows-kt/api-docs/' 55 | - Projects using this library: 'projects-using-this-library.md' 56 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "automerge": true 7 | } 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "github-workflows-kt-monorepo" 2 | 3 | apply(from = "./buildSrc/repositories.settings.gradle.kts") 4 | 5 | include( 6 | "github-workflows-kt", 7 | "action-binding-generator", 8 | "action-updates-checker", 9 | "maven-binding-builder", 10 | "jit-binding-server", 11 | "shared-internal", 12 | "code-generator", 13 | ) 14 | 15 | plugins { 16 | id("com.gradle.develocity") version "4.0.2" 17 | } 18 | 19 | dependencyResolutionManagement { 20 | @Suppress("UnstableApiUsage") // Central declaration of repositories is an incubating feature 21 | repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) 22 | } 23 | 24 | develocity { 25 | buildScan { 26 | termsOfUseUrl = "https://gradle.com/terms-of-service" 27 | termsOfUseAgree = "yes" 28 | publishing.onlyIf { 29 | System.getenv("GITHUB_ACTIONS") == "true" && it.buildResult.failures.isNotEmpty() 30 | } 31 | } 32 | } 33 | 34 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 35 | -------------------------------------------------------------------------------- /shared-internal/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | buildsrc.convention.`kotlin-jvm` 3 | buildsrc.convention.publishing 4 | 5 | kotlin("plugin.serialization") 6 | id("io.gitlab.arturbosch.detekt") 7 | } 8 | 9 | group = rootProject.group 10 | version = rootProject.version 11 | 12 | dependencies { 13 | api("io.arrow-kt:arrow-core:2.1.2") 14 | // we cannot use a BOM due to limitation in kotlin scripting when resolving the transitive KMM variant dependencies 15 | // note: see https://youtrack.jetbrains.com/issue/KT-67618 16 | api("io.ktor:ktor-client-core:3.1.3") 17 | implementation("io.ktor:ktor-client-cio:3.1.3") 18 | implementation("io.ktor:ktor-client-content-negotiation:3.1.3") 19 | implementation("io.ktor:ktor-client-logging:3.1.3") 20 | implementation("io.ktor:ktor-serialization-kotlinx-json:3.1.3") 21 | implementation("io.github.oshai:kotlin-logging:7.0.7") 22 | implementation("com.auth0:java-jwt:4.5.0") 23 | implementation("org.kohsuke:github-api:1.327") 24 | 25 | // It's a workaround for a problem with Kotlin Scripting, and how it resolves 26 | // conflicting versions: https://youtrack.jetbrains.com/issue/KT-69145 27 | // As of adding it, ktor-serialization-kotlinx-json depends on kotlinx-io-core:0.4.0, 28 | // and because of how Scripting resolves the libraries, 0.4.0 is used in the script, 29 | // leading to a runtime failure of not being able to find a class or a function. 30 | // I'm bumping kotlinx-io to 0.6.0 in kotlinx.serialization here: https://github.com/Kotlin/kotlinx.serialization/pull/2933 31 | // Here's a ticket to remember to remove this workaround: https://github.com/typesafegithub/github-workflows-kt/issues/1832 32 | runtimeOnly("org.jetbrains.kotlinx:kotlinx-io-core:0.7.0") 33 | 34 | testImplementation("io.kotest.extensions:kotest-extensions-mockserver:1.3.0") 35 | testImplementation("org.slf4j:slf4j-simple:2.0.17") 36 | } 37 | -------------------------------------------------------------------------------- /shared-internal/src/main/kotlin/io/github/typesafegithub/workflows/shared/internal/GitHubApp.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.shared.internal 2 | 3 | import com.auth0.jwt.JWT 4 | import com.auth0.jwt.algorithms.Algorithm 5 | import io.github.oshai.kotlinlogging.KotlinLogging.logger 6 | import org.kohsuke.github.GHAppInstallationToken 7 | import org.kohsuke.github.GitHubBuilder 8 | import java.security.KeyFactory 9 | import java.security.interfaces.RSAPrivateKey 10 | import java.security.spec.PKCS8EncodedKeySpec 11 | import java.time.Instant 12 | import java.util.Base64 13 | 14 | private val logger = logger { } 15 | 16 | private var cachedAccessToken: GHAppInstallationToken? = null 17 | 18 | /** 19 | * Returns an installation access token for the GitHub app, usable with API call to GitHub. 20 | * If `null` is returned, it means that the environment wasn't configured to generate the token. 21 | */ 22 | @Suppress("ReturnCount") 23 | fun getInstallationAccessToken(): String? { 24 | if (cachedAccessToken?.isExpired() == false) return cachedAccessToken!!.token 25 | val jwtToken = generateJWTToken() ?: return null 26 | val installationId = System.getenv("APP_INSTALLATION_ID") ?: return null 27 | 28 | val gitHubApp = GitHubBuilder().withJwtToken(jwtToken).build().app 29 | 30 | cachedAccessToken = 31 | gitHubApp.getInstallationById(installationId.toLong()).createToken().create().also { 32 | logger.info { "Fetched new GitHub App installation access token for repository ${it.repositorySelection}" } 33 | } 34 | return cachedAccessToken!!.token 35 | } 36 | 37 | private fun GHAppInstallationToken.isExpired() = Instant.now() >= expiresAt.toInstant() 38 | 39 | @Suppress("ReturnCount", "MagicNumber") 40 | private fun generateJWTToken(): String? { 41 | val key = loadRsaKey() ?: return null 42 | val appClientId = System.getenv("APP_CLIENT_ID") ?: return null 43 | val algorithm = Algorithm.RSA256(null, key) 44 | val now = Instant.now() 45 | return JWT 46 | .create() 47 | .withIssuer(appClientId) 48 | .withIssuedAt(now.minusMinutes(1)) 49 | .withExpiresAt(now.plusMinutes(9)) 50 | .sign(algorithm) 51 | } 52 | 53 | private fun loadRsaKey(): RSAPrivateKey? { 54 | val privateKey = System.getenv("APP_PRIVATE_KEY") ?: return null 55 | val filtered = 56 | privateKey 57 | .replace("-----BEGIN PRIVATE KEY-----", "") 58 | .replace("-----END PRIVATE KEY-----", "") 59 | .replace("\\s".toRegex(), "") 60 | val keyBytes = Base64.getDecoder().decode(filtered) 61 | val keySpec = PKCS8EncodedKeySpec(keyBytes) 62 | val keyFactory = KeyFactory.getInstance("RSA") 63 | return keyFactory.generatePrivate(keySpec) as RSAPrivateKey 64 | } 65 | 66 | @Suppress("MagicNumber") 67 | private fun Instant.minusMinutes(minutes: Long): Instant = minusSeconds(minutes * 60) 68 | 69 | @Suppress("MagicNumber") 70 | private fun Instant.plusMinutes(minutes: Long): Instant = plusSeconds(minutes * 60) 71 | -------------------------------------------------------------------------------- /shared-internal/src/main/kotlin/io/github/typesafegithub/workflows/shared/internal/GithubUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.shared.internal 2 | 3 | import io.github.oshai.kotlinlogging.KotlinLogging.logger 4 | 5 | private val logger = logger { } 6 | 7 | /** 8 | * Returns a token that should be used to make authorized calls to GitHub, 9 | * or null if no token was configured. 10 | * The token may be of various kind, e.g. a Personal Access Token, or an 11 | * Application Installation Token. 12 | */ 13 | fun getGithubAuthTokenOrNull(): String? { 14 | val installationAccessToken = 15 | runCatching { 16 | getInstallationAccessToken() 17 | }.onFailure { logger.warn(it) { "Failed to get GitHub App Installation token." } }.getOrNull() 18 | 19 | return installationAccessToken ?: System.getenv("GITHUB_TOKEN") 20 | } 21 | 22 | /** 23 | * Returns a token that should be used to make authorized calls to GitHub, 24 | * or throws an exception if no token was configured. 25 | * The token may be of various kind, e.g. a Personal Access Token, or an 26 | * Application Installation Token. 27 | */ 28 | fun getGithubAuthToken(): String = getGithubAuthTokenOrNull() ?: error(ERROR_NO_CONFIGURATION) 29 | 30 | private val ERROR_NO_CONFIGURATION = 31 | """ 32 | Missing environment variables for generating an auth token. There are two options: 33 | 1. Create a personal access token at https://github.com/settings/tokens. 34 | The token needs to have public_repo scope. Then, set it in `GITHUB_TOKEN` env var. 35 | With this approach, listing versions for some actions may not work. 36 | 2. Create a GitHub app, and generate a private key. Then, set it in `APP_PRIVATE_KEY` env var. 37 | With this approach, listing versions for all actions works. 38 | """.trimIndent() 39 | -------------------------------------------------------------------------------- /shared-internal/src/main/kotlin/io/github/typesafegithub/workflows/shared/internal/PathUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.shared.internal 2 | 3 | import java.nio.file.Path 4 | import kotlin.io.path.absolute 5 | import kotlin.io.path.exists 6 | 7 | fun Path.findGitRoot(): Path = 8 | generateSequence(this.absolute()) { it.parent } 9 | .firstOrNull { it.resolve(".git").exists() } 10 | ?: error("could not find a git root from ${this.absolute()}") 11 | -------------------------------------------------------------------------------- /shared-internal/src/main/kotlin/io/github/typesafegithub/workflows/shared/internal/model/Version.kt: -------------------------------------------------------------------------------- 1 | package io.github.typesafegithub.workflows.shared.internal.model 2 | 3 | import java.time.ZonedDateTime 4 | 5 | data class Version( 6 | val version: String, 7 | private val dateProvider: suspend () -> ZonedDateTime? = { null }, 8 | ) : Comparable { 9 | private val versionParts: List = version.removePrefix("v").removePrefix("V").split('.') 10 | private val versionIntParts: List = versionParts.map { it.toIntOrNull() } 11 | val major: Int = versionIntParts.getOrNull(0) ?: 0 12 | val minor: Int = versionIntParts.getOrNull(1) ?: 0 13 | val patch: Int = versionIntParts.getOrNull(2) ?: 0 14 | 15 | @Suppress("ReturnCount") 16 | override fun compareTo(other: Version): Int { 17 | versionParts.forEachIndexed { i, part -> 18 | val otherPart = other.versionParts.getOrNull(i) 19 | if (otherPart == null) return 1 20 | val intPart = versionIntParts[i] 21 | val otherIntPart = other.versionIntParts[i] 22 | if ((intPart == null) && (otherIntPart == null)) { 23 | val comparison = part.compareTo(otherPart) 24 | if (comparison != 0) return comparison 25 | } else if (intPart == null) { 26 | return -1 27 | } else if (otherIntPart == null) { 28 | return 1 29 | } else { 30 | val comparison = intPart.compareTo(otherIntPart) 31 | if (comparison != 0) return comparison 32 | } 33 | } 34 | if (versionParts.size < other.versionParts.size) return -1 35 | return version.compareTo(other.version) 36 | } 37 | 38 | override fun equals(other: Any?): Boolean = this.compareTo(other as Version) == 0 39 | 40 | override fun toString(): String = version 41 | 42 | fun isMajorVersion(): Boolean = versionIntParts.singleOrNull() != null 43 | 44 | suspend fun getReleaseDate() = dateProvider() 45 | } 46 | --------------------------------------------------------------------------------