Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write GitHub Actions workflow files in Kotlin instead of YAML #1630

Open
wants to merge 30 commits into
base: master
Choose a base branch
from

Conversation

Vampire
Copy link
Member

@Vampire Vampire commented Apr 12, 2023

For now a 1:1 translation from previous Yaml.

@Vampire Vampire force-pushed the kotlin-workflow-files branch 2 times, most recently from 4f2aa09 to c3ed268 Compare April 12, 2023 02:44
@Vampire Vampire requested a review from leonard84 April 12, 2023 02:46
@codecov
Copy link

codecov bot commented Apr 12, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 81.82%. Comparing base (2c7db77) to head (2ffa401).
Report is 107 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1630      +/-   ##
============================================
+ Coverage     80.44%   81.82%   +1.38%     
- Complexity     4337     4574     +237     
============================================
  Files           441      446       +5     
  Lines         13534    14342     +808     
  Branches       1707     1814     +107     
============================================
+ Hits          10888    11736     +848     
+ Misses         2008     1939      -69     
- Partials        638      667      +29     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@Vampire
Copy link
Member Author

Vampire commented Apr 12, 2023

Well, almost 1:1, I added some names where only IDs were present. Due to that the required checks now do not match anymore as they are considering the name, not the ID.

Copy link
Member

@leonard84 leonard84 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not seeing the advantages yet.

build.gradle Outdated
Comment on lines 467 to 507
configurations {
kotlinCompilerClasspath {
canBeConsumed = false
}
kotlinScriptClasspath {
canBeConsumed = false
}
}

dependencies {
kotlinCompilerClasspath 'org.jetbrains.kotlin:kotlin-compiler:1.8.20'
kotlinCompilerClasspath 'org.jetbrains.kotlin:kotlin-scripting-compiler:1.8.20'
kotlinScriptClasspath('org.jetbrains.kotlin:kotlin-main-kts:1.8.20') { transitive = false }
}

def preprocessWorkflows = tasks.register('preprocessWorkflows')
file('.github/workflows').eachFileMatch(~/.*\.main\.kts$/) { workflowScript ->
def workflowName = workflowScript.name - ~/\.main\.kts$/
def pascalCasedWorkflowName = workflowName
.replaceAll(/-\w/) { it[1].toUpperCase() }
.replaceFirst(/^\w/) { it[0].toUpperCase() }
def preprocessWorkflow = tasks.register("preprocess${pascalCasedWorkflowName}Workflow", JavaExec) {
inputs
.file(workflowScript)
.withPropertyName('workflowScript')
outputs
.file(new File(workflowScript.parent, "${workflowName}.yaml"))
.withPropertyName('workflowFile')

javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(17)
}
classpath(configurations.kotlinCompilerClasspath)
mainClass = 'org.jetbrains.kotlin.cli.jvm.K2JVMCompiler'
args('-no-stdlib', '-no-reflect')
args('-classpath', configurations.kotlinScriptClasspath.asPath)
args('-script', workflowScript.absolutePath)
}
preprocessWorkflows.configure {
dependsOn(preprocessWorkflow)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole thing should be extracted into build-logic this files is already too bloated.

Copy link
Member

@leonard84 leonard84 Apr 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can we add the validation here and just have on job that validates all workflows instead of having it inline of each job? Changes to the release job wouldn't be validated in the PR otherwise.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole thing should be extracted into build-logic this files is already too bloated.

Yep, no problem, moved to a build-logic subproject.

Also can we add the validation here and just have on job that validates all workflows instead of having it inline of each job?

Well, yes and no.
Indeed we should have a check that verifies all YAMLs for PRs and branches as not all run always like the "release" one.
I added this now.
You can disable the per-workflow consistency check that is generated, but I would recommend to keep it there.
The YAML is basically just the output format of the Kotlin scripts and that it is there additionally should not be too much of a problem.
Until https://github.com/orgs/community/discussions/15904 is considered and realized, you just have to run the scripts manually and check in the result and thus need the consistency check.
So I'd suggest to keep the per-file consistency check there, as then the jobs defined in the respective file are also always only executed if the consistency check was successful.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep the .yml extension for the generated files, so that we can actually see the diff?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you can manually configure the target file name.
I changed it to ...yml, but maybe we want to return to the default after you verified the changes to not have the unnecessary configuration?

* limitations under the License.
*/

@file:DependsOn("io.github.typesafegithub:github-workflows-kt:0.40.1")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not add this to the kotlinScriptClasspath instead of duplicating it in every .kts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Kotlin .main.kts script is a standalone utility that can run on its own.
The native way to call them is to have Kotlin installed and just execute the script either explicitly with Kotlin CLI, or if you are using some sh derivative by executing the script.

The Gradle task is just a way I came up with to be able to execute the scripts as Gradle tasks.

The @DependsOn within the script is needed so that the normal way to run it works, that is for example also used by the consistency checks and also is faster than ramping up Gradle, which I just added as convenient launcher without the need to have Kotlin installed.

Also the IntelliSense in the IDE would not work if the @DependsOn were not there as it then would not know about that dependency.

Regarding the duplication, you could actually drag out common things like defining the SetupBuildEnv class or also the @DependsOn line to a common script that you then include with @file:Import. Unfortunately, this approach has still some bugs in Kotlin. For example you then sometimes loose IntelliSense completely and if there is a change in the common script, you get strange non-obvious compilation errors that you can only fix (as far as I know) by deleting the .main.kts compilation cache locally. Hence I did not yet use that approach in Spock. But you could have a look at it at https://github.com/Vampire/setup-wsl/tree/master/.github/workflows where I define some common logic in workflow-with-copyright.main.kts and live with the consequences.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would be an acceptable downside to get rid of the duplication. Given that those file are not frequently modified.
I'd document the caveat and fix in a readme in the workflows folder.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I refactored some things out to a common.main.kts and added the readme, also describing the approach and different options to generate the YAML files, together with the caveats.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking more about this and the issues with this approach that you documented.

A Kotlin .main.kts script is a standalone utility that can run on its own. The native way to call them is to have Kotlin installed and just execute the script either explicitly with Kotlin CLI, or if you are using some sh derivative by executing the script.

Do we really need the standalone functionality? Personally, I probably wouldn't run it that way and it seems the downsides are pretty heavy. We could replace the consistency check with a gradle task, or just rely on the GH action to validate the state.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems the downsides are pretty heavy.

The downsides are not related to being able to run it standalone.
The first two are bugs of the Kotlin IntelliJ plugin which are happening when editing the scripts. How you run them is irrelevant.
But in one of the according issues it was just said that they plan to improve the whole experience, so hopefully this gets fixed in forseeable future.

The third point is a bug in Kotlin itself, either the compiler or the runtime, I don't know. Indeed there would be another mitigation option, especially as they are not run often. You can disable the compilation cache by setting an evironment variable, so for running through Gradle I could just always disable the compilation cache and that would automatically mitigate the problem there. On GHA it is not a problem anyway as it runs on ephemeral environment so there is no compilation cache used anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it to the Gradle tasks and mentioned it in the mitigation strategies should someone wish to use one of the other ways.

build.gradle Outdated Show resolved Hide resolved
@Vampire Vampire force-pushed the kotlin-workflow-files branch 4 times, most recently from b8fd23e to 9267bae Compare April 17, 2023 08:35
@Vampire
Copy link
Member Author

Vampire commented Apr 17, 2023

Not seeing the advantages yet.

Well, noone really likes reading or writing YAML. :-D
And especially with such a complex DSL like GitHub workflow files.
You always have to read how you can do what where how and have to trial and error.
By using this library, you have a proper type-safe DSL with IntelliSense in the IDE that makes it nice to write and read GHA workflows.

You can also easily reuse common logic as you have a turing-complete language at hand.
So for example the logic in the custom local setup-build-env and setup-jdks workflows could just be done in a common Kotlin script, if one would be used (read above for why none is used so far).

You could then for example also centrally define the variants and Java versions, or even the complete test matrix and reuse it.

And where it also comes in handy is, for the changes in #1629 as there I can then easily change the script to calculate the size of the Matrix and set the after_n_builds accordingly without the risk of forgetting to update it.

@Vampire Vampire requested a review from leonard84 April 17, 2023 09:12
@Vampire
Copy link
Member Author

Vampire commented Apr 18, 2023

I now added another commit, that leverages the fact we have a programming language at hand and uses it for the variants and java versions handling, that also shows that it is really useful. :-)

@Vampire
Copy link
Member Author

Vampire commented Apr 19, 2023

And another commit added, so that the build script and the workflow generation script use one common source for the Java versions and variants. :-)

I think I'm out of improvement ideas for now, so review-on. :-)

@Vampire Vampire force-pushed the kotlin-workflow-files branch 2 times, most recently from e6d19ea to e65cca2 Compare April 19, 2023 13:50
@Vampire Vampire changed the title Write GitHub Actions workflow files in Kotlin instead of Yaml Write GitHub Actions workflow files in Kotlin instead of YAML Apr 19, 2023
Copy link
Member

@leonard84 leonard84 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By using this library, you have a proper type-safe DSL with IntelliSense in the IDE that makes it nice to write and read GHA workflows.

Unless, it is not supported, then you end up with _customArguments and unsightly mapOf, which read much worse through all the added noise IMHO.

build.gradle Outdated Show resolved Hide resolved
id = "build-and-verify",
name = "Build and Verify",
runsOn = RunnerType.Custom(expr("matrix.os")),
_customArguments = mapOf(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't really nice to read

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, now with the common file I also added some abstration and centralization, should be much nicer now hopefully.

.github/workflows/branches-and-prs.main.kts Outdated Show resolved Hide resolved
.github/workflows/branches-and-prs.main.kts Outdated Show resolved Hide resolved
.github/workflows/branches-and-prs.main.kts Outdated Show resolved Hide resolved
.github/workflows/release.main.kts Outdated Show resolved Hide resolved
* limitations under the License.
*/

@file:DependsOn("io.github.typesafegithub:github-workflows-kt:0.40.1")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would be an acceptable downside to get rid of the duplication. Given that those file are not frequently modified.
I'd document the caveat and fix in a readme in the workflows folder.

@Vampire Vampire requested a review from leonard84 June 1, 2023 17:04
@Vampire Vampire force-pushed the kotlin-workflow-files branch 2 times, most recently from 8f578ed to 69bfb43 Compare June 6, 2023 13:04
Comment on lines 112 to 120
excludes = axes.javaVersions
.filter { it.toInt() >= 17 }
.map { javaVersion ->
mapOf(
"os" to "ubuntu-latest",
"variant" to "2.5",
"java" to javaVersion
)
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we have something more intuitive, now that we have a programming language?
For example having a predicate that gets the each combination and can decide to exclude it { it.java >= 17 && variant == "2.5" }?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely :-)

Comment on lines 115 to 119
mapOf(
"os" to "ubuntu-latest",
"variant" to "2.5",
"java" to javaVersion
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we not use a data class for these?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, why not. :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants