Problem
sbt-typelevel's generated Test job emits a step named "Check headers and formatting" that runs sbt commands like:
sbt '++ ${{ matrix.scala }}' headerCheckAll scalafmtCheckAll 'project /' scalafmtSbtCheck
Plugins and projects that contribute additional lint-style checks (custom formatters, schema validators, license linters, etc.) want to fold their check into this same step so it runs alongside the existing ones — same matrix cell, same conditions, same "lint failures show up here" semantics. There's no first-class way to do that today; the only option is a fragile rewrite of githubWorkflowBuild:
ThisBuild / githubWorkflowBuild ~= {
_.map {
case step: WorkflowStep.Sbt if step.name == Some("Check headers and formatting") =>
step.withCommands(step.commands :+ "myLintCheckAll")
case other => other
}
}
This is nine lines per plugin, repeats the magic step-name string at every call site, and breaks if upstream renames the step or splits it. It also doesn't compose: two plugins both rewriting githubWorkflowBuild need to be careful about order and not stomping each other.
Proposed shape
A public setting that's just a list of extra commands appended into the existing lint step:
val tlCiLintCommands = settingKey[Seq[String]](
"Additional sbt commands appended to the 'Check headers and formatting' step"
)
// existing default rendering becomes something like:
WorkflowStep.Sbt(
List(
s"$$ $${{ matrix.scala }}",
"headerCheckAll",
"scalafmtCheckAll",
"project /",
"scalafmtSbtCheck",
) ++ tlCiLintCommands.value,
name = Some("Check headers and formatting"),
cond = ...,
)
User code becomes one line:
ThisBuild / tlCiLintCommands += "smithyFmtCheckAll"
…and plugins can opt projects in by default:
override def projectSettings = Seq(
tlCiLintCommands += "smithyFmtCheckAll",
)
+='d sequences compose naturally across plugins and project settings, so there's no ordering footgun.
Why it's worth doing
- Removes a copy-paste recipe from documentation. We currently tell
SmithyFormatPlugin users (in polyvariant/smithy-trait-codegen-scala) to write the nine-line githubWorkflowBuild ~= { … } snippet by hand — every reader has to understand step matching, WorkflowStep.Sbt, and withCommands just to bolt one task onto CI.
- Plays nicely with autoplugins. A plugin that wants to opt projects into a check on CI can do so transparently via
projectSettings, with users' explicit additions composing on top.
- The current pattern is brittle to upstream changes (step name, step splitting) — a named setting decouples user code from the rendering layout.
Happy to send a PR if the shape lands.
Problem
sbt-typelevel's generated
Testjob emits a step named"Check headers and formatting"that runs sbt commands like:Plugins and projects that contribute additional lint-style checks (custom formatters, schema validators, license linters, etc.) want to fold their check into this same step so it runs alongside the existing ones — same matrix cell, same conditions, same "lint failures show up here" semantics. There's no first-class way to do that today; the only option is a fragile rewrite of
githubWorkflowBuild:This is nine lines per plugin, repeats the magic step-name string at every call site, and breaks if upstream renames the step or splits it. It also doesn't compose: two plugins both rewriting
githubWorkflowBuildneed to be careful about order and not stomping each other.Proposed shape
A public setting that's just a list of extra commands appended into the existing lint step:
User code becomes one line:
…and plugins can opt projects in by default:
+='d sequences compose naturally across plugins and project settings, so there's no ordering footgun.Why it's worth doing
SmithyFormatPluginusers (in polyvariant/smithy-trait-codegen-scala) to write the nine-linegithubWorkflowBuild ~= { … }snippet by hand — every reader has to understand step matching,WorkflowStep.Sbt, andwithCommandsjust to bolt one task onto CI.projectSettings, with users' explicit additions composing on top.Happy to send a PR if the shape lands.