-
Notifications
You must be signed in to change notification settings - Fork 470
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
base: master
Are you sure you want to change the base?
Conversation
4f2aa09
to
c3ed268
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅
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. |
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. |
There was a problem hiding this 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
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) | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
b8fd23e
to
9267bae
Compare
Well, noone really likes reading or writing YAML. :-D You can also easily reuse common logic as you have a turing-complete language at hand. 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 |
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. :-) |
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. :-) |
e6d19ea
to
e65cca2
Compare
e004e5f
to
e65cca2
Compare
e65cca2
to
8f5f426
Compare
There was a problem hiding this 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.
id = "build-and-verify", | ||
name = "Build and Verify", | ||
runsOn = RunnerType.Custom(expr("matrix.os")), | ||
_customArguments = mapOf( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
* limitations under the License. | ||
*/ | ||
|
||
@file:DependsOn("io.github.typesafegithub:github-workflows-kt:0.40.1") |
There was a problem hiding this comment.
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.
2d52d58
to
8dadbc2
Compare
8f578ed
to
69bfb43
Compare
.github/workflows/common.main.kts
Outdated
excludes = axes.javaVersions | ||
.filter { it.toInt() >= 17 } | ||
.map { javaVersion -> | ||
mapOf( | ||
"os" to "ubuntu-latest", | ||
"variant" to "2.5", | ||
"java" to javaVersion | ||
) | ||
}, |
There was a problem hiding this comment.
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" }
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely :-)
.github/workflows/common.main.kts
Outdated
mapOf( | ||
"os" to "ubuntu-latest", | ||
"variant" to "2.5", | ||
"java" to javaVersion | ||
) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, why not. :-)
69bfb43
to
4ae5422
Compare
a2e391d
to
793dffb
Compare
793dffb
to
73058a6
Compare
9e5c026
to
1520e8d
Compare
7ace872
to
949610b
Compare
949610b
to
9ad73eb
Compare
For now a 1:1 translation from previous Yaml.