-
Notifications
You must be signed in to change notification settings - Fork 49
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
Add CI command alias derived from the sbt-github-actions matrix #210
base: series/0.4
Are you sure you want to change the base?
Add CI command alias derived from the sbt-github-actions matrix #210
Conversation
case (k, v) => | ||
val renderedCond = s"matrix.$k == '$v'" | ||
|
||
command.cond.forall { cond => |
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.
Is it likely that the condition itself might refer to a matrix variable? Perhaps cond
needs to have its matrix vars replaced too.
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, that's very likely. In fact we do it from sbt-typelevel itself.
cond = Some("matrix.project == 'rootJS'") |
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.
Ah that should work OK - what I was worried about was things like
Some("something == '${{ matrix.project }}'")
In that scenario we would need to make sure to replace any Github Actions ${{ }}
variables before checking the conditions.
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.
Hmm, I might be confused but in conditions you should assume everything is wrapped in ${{ }}
. Like, the matrix.project
in that condition should definitely be substituted, no? :)
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.
https://docs.github.com/en/actions/learn-github-actions/expressions
When you use expressions in an if conditional, you may omit the expression syntax (${{ }}) because GitHub automatically evaluates the if conditional as an expression.
step <- workflowSteps.collect { | ||
case sbt: WorkflowStep.Sbt if matchingCondition(sbt, matrixValues) => sbt | ||
} | ||
command <- sbtStepPreamble ++ step.commands |
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.
so one of the annoying things is that sbt is stateful. So I wonder if we need to reload
or something between steps as a palate-cleanser ...
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.
Hmm I think that should be OK - from what I understand State
is the global command loop. Anything that you feed into there is essentially interpreted in the same way as if you ran it interactively - so set
commands such as setting JsEnv should work fine as long as the key is scoped correctly.
As long as there is nothing that happens as part of those commands that modifies the project configuration on disk, that statefulness should not necessarily cause a problem, which I think is the case for something like the ci
command where we do lots of linting, compiling and tests.
However, my understanding of sbt is not 100% so take that with a pinch of salt🤞
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.
For sure! Except when these steps run in CI it's not part of a single interactive session—each workflow step reboots sbt essentially.
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.
Ahhhh I see, so you are concerned about potential session value leakage in some of the existing commands. FWIW I think interspersing reload
would make things extremely slow!
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.
Okay this is cool. Just a brief comment though: is
Global
even going to work here? Most of the github actions configuration is inThisBuild
.
Yeah I wondered about that too, however onLoad
and onUnload
actually seem to be undefined in other scopes - I guess it makes sense that command aliases are global. Those are the scopes that are used in the source of addCommandAlias
from what I can tell.
Remind me not to try replying to comments on my phone, who knows how this ended up here!
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.
FWIW I think interspersing reload would make things extremely slow!
Oh, absolutely agree! But the if the goal is to faithfully replicate CI then I really don't see another way.
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.
Hmm, what if we do session clear
instead of a full reload? That would revert any settings manipulated using set
.
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.
Oh yeah, that's probably just as good. Not sure what reload
does that session clear
doesn't (besides reload the build config which obviously doesn't apply here).
@@ -46,12 +46,101 @@ object TypelevelCiPlugin extends AutoPlugin { | |||
cond = Some(primaryJavaCond.value) | |||
) | |||
), | |||
githubWorkflowJavaVersions := Seq(JavaSpec.temurin("8")) | |||
githubWorkflowJavaVersions := Seq(JavaSpec.temurin("8")), | |||
GlobalScope / Keys.onLoad := { |
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 actually don't know what these GlobalScope / Keys.onLoad
incantations are for? :)
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.
Those are sbt's global load hooks - they add the alias when all of the project's keys have loaded and remove it when unloading the projects. Setting up these specific hooks is what addCommandAlias
does under the hood. If you try to use onLoad
in the default scope you get 'reference to undefined setting' unfortunately.
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.
Right, it's the conditionals that make this problem hard. I think if we want to do this right and not be brittle, we really need a proper interpreter for GH conditional statements without requiring them to be formatted in a particular way.
Okay this is cool. Just a brief comment though: is |
Yeah I wondered about that too, however it seems that command aliases are global - onLoad and onUnload actually seem to be defined only in |
Yeah, either that or make the representation of the conditions more restrictive in sbt-github-actions' AST. I think we could fairly easily support a subset of Github Actions expressions but we would most likely want to avoid implementing anything more than literals and operators, as I imagine that the rest is quite a large moving target. |
I don't think we can easily do that in a backwards-compatible way. Actually, overall there don't seem to be many expression operators/functions. But to start with if we can interpret https://docs.github.com/en/actions/learn-github-actions/expressions |
89dab7b
to
450c07e
Compare
I've been thinking about this a bit... If we get into evaluating GitHub Actions expressions there's quite a lot involved; we'd need an AST, a parser, an evaluator, and I guess if we are investing that much effort we probably want to do things properly and worry about the differences in the textual representations of literals on JVM vs JS (escape sequences etc.) and the semantics that GitHub Actions has for its expressions, i.e. the way it deals with type coercions and the fact that string comparisons are case insensitive. However there is a super gross thing that 80% works for the cases we care about, namely using a Javascript engine like Rhino: def toJsLiteral(mapping: Map[String, String]): String = {
mapping
.view
.map {
case (k, v) =>
s""" "$k": "$v""""
}
.mkString("{\n", ",\n", "\n}\n")
}
private def matchingCondition(command: WorkflowStep.Sbt, matrix: Map[String, String]) = {
val context = Context.enter()
try {
command.cond.fold(true) { cond =>
val scope = context.initStandardObjects()
val script = s"""|var matrix = ${toJsLiteral(matrix)};
|var env = ${toJsLiteral(command.env)}
|java.lang.System.out.println('matrix = ' + JSON.stringify(matrix));
|java.lang.System.out.println('env = ' + JSON.stringify(env));
|java.lang.System.out.println("cond = $cond");
|var result = $cond;
|java.lang.System.out.println('result = ' + JSON.stringify(result));""".stripMargin
val result = context.evaluateString(
scope,
script,
"",
1,
null
)
scope.get("result") match {
case lang.Boolean.FALSE => false
case lang.Boolean.TRUE => true
case _ => true
}
}
} finally {
Context.exit()
}
} ====>
I feel like the right answer here is probably somewhere in between these two extremes but I'm not sure I know what that middle ground is. Do you guys have any ideas? |
Ooh, that is grossly brilliant! That's super tempting tbh 😁 In fact, there are even JS libs that can actually evaluate these expressions, e.g.: |
This adds a
ci
command derived from the sbt-github-actions configuration - perhaps thatci
command should be namespaced somewhat?tlCi
ortypelevelCi
?I think this does what folks want but it would be good to get a few eyes on this to sense check it. 😆
In general this feels a little fragile because a lot of the things we need have become
String
s by the time we need to evaluate them to build the sbt command.Unfortunately
addCommandAlias
doesn't work very well with dynamic data - it produces multiple keys in one goSeq[Setting[State => State]]
, and sbt would not let me use other key.value
s within the command definition even while wrapped withinDef.settings
. What I have done replicates whataddCommandAlias
does under the hood.Contributed on behalf of the @opencastsoftware open source team. 👋