Validation
zipx fails early: a bad name fails compile or zipxWorkflowGenerate, not a green CI run that did the wrong thing. The
rest of this page is the catalog of checks.
Three moments, earliest first:
The one rule to remember: Foo("literal") is checked while your build compiles. Foo.make(runtimeValue) returns an
Either you carry. Passing a non-literal to the first is itself a compile error telling you to use the second, so there
is no way to skip the check by accident.
Compile time: every named value
Every one of these is a neotype wrapper with an inline apply, so a bad
literal fails your build, not your CI run:
| Module | Types |
|---|---|
zipx-shell | ShText, SquoteText, ParamText, ScriptLine, VarName, GlobPattern, ProgramName, HeredocTag, ExitCode, FileDescriptor |
zipx-workflow | JobId, StepId, SecretName, EnvName, OutputName, MatrixAxis, ContextPath, ActionRef, EventName, FunctionName, ExprLiteral, RawExpr, CronHour, CronMinute, CronExpr |
zipx-core | ModuleId, CapabilityName, TargetName, WorkflowName, RunnerOs, JdkVersion, NodeVersion, SbtCommandText |
zipx-aws | AwsAccountId, AwsRegion, EcrRepository, ImageTag |
Structure is checked the same way, by making the wrong shape unrepresentable rather than by rejecting it later:
StepBuilder cannot produce a step with both uses: and run:, or with: on a run: step, so the two rules
[[zipx.workflow.Step.validate]] exists for are unreachable from Step.run / Step.uses. EcrRegistry has no
constructor without a region, which is why a generated login step cannot omit aws-region. See Shell and steps.
List(
s"a module id GitHub would reject: ${ModuleId.make("café").isLeft}",
s"a capability name with a slash: ${CapabilityName.make("docker/stg").isLeft}",
s"an unpinned action ref: ${ActionRef.make("actions/checkout").isLeft}",
s"a reserved secret prefix: ${SecretName.make("GITHUB_FOO").isLeft}",
s"an account id one digit short: ${AwsAccountId.make("11112222333").isLeft}",
s"a branch name used as an image tag: ${ImageTag.make("main-feat/x-abc123").isLeft}",
).mkString("\n")a module id GitHub would reject: true
a capability name with a slash: true
an unpinned action ref: true
a reserved secret prefix: true
an account id one digit short: true
a branch name used as an image tag: trueTyped command settings
zipxTestTask, zipxPublishTask and zipxCacheRehydrateTask are SettingKey[SbtCommand]. Prefer real keys:
zipxTestTask := zipxTasks.of(testFull) // plugin default
zipxCacheRehydrateTask := zipxTasks.of(compile)
SbtCommand.raw("…") remains the escape hatch for free text. Declared command names (zipxTasks.of(someCommand),
Coverage's coverage alias, sonaRelease) are checked at generate time when zipxCheckCommandNames is true (default).
See Composing sbt commands.
Generate time: everything assembled from more than one file
A build's CI config is spread across build.sbt, project/*.scala, and packs. Nothing checked at a
literal can see the combination, so [[zipx.core.Planner]] checks that, and zipxWorkflowGenerate fails rather than
writing a file:
| Check | What it catches |
|---|---|
validateCapabilities: Gate.AffectedOnly | an unimplemented seam that would silently behave as Gate.Always |
validateCapabilities: needsCapabilities cycle | two capabilities each waiting on the other |
validateWorkflowCall | container: / services: beside workflowCall, which GitHub rejects |
validateSharedTargets | a per-destination condition or environment on a SharedJob capability |
validateSatisfiable | a gate/condition conjunction that can never be true (see Job conditions) |
ModuleGraph.make | a dependency cycle, or two modules with one id |
ModuleId.make on every sbt project id | a project id sbt allows and a GitHub job id does not |
Step.validate / YamlPrinter.problem at render | a hand-built step, or content YAML would mangle |
Action constructor / leftover pin YAML | a short SHA, a leftover .github/zipx/action-pins.yml, a duplicate Action name |
leftover zipx-scala-steward.yml | Fail (default) or Warn(reason); the replacement companion is zipx-version-updates.yml |
| Ship membership / library publish topology | a publishing module with no row, Aggregate/OnReleaseTag library publish when ships are present; see Independent versions |
A leftover pin YAML fails generate with the Action(...) vals to paste. Catalog rows overlay jar defaults. See
Action pins.
{
val a = Capability.publish.copy(name = CapabilityName("a"), needsCapabilities = List(CapabilityName("b")))
val b = Capability.publish.copy(name = CapabilityName("b"), needsCapabilities = List(CapabilityName("a")))
val affects = Capability.publish.copy(gate = Gate.AffectedOnly)
def failure(caps: Capability*): String =
scala.util.Try(DocsRender.plan(caps*)).fold(_.getMessage, _ => "planned (no error)")
List(
failure(a, b),
failure(affects),
ActionPinFile.parse("chekout: actions/checkout@v7.0.1").fold(identity, _ => "parsed"),
ActionPinFile.parse("checkout: actions/setup-java@v5").fold(identity, _ => "parsed"),
).mkString("\n")
}zipx: needsCapabilities cycle among a, b
zipx: Gate.AffectedOnly is not implemented, so capabilities publish would silently run on every event. Affected-gating is controlled by zipxAffectedOnPR / zipxAffectedOnPush / zipxAffectedPublish / zipxAffectedDeploy on Graph capabilities, not by Gate. Use Gate.Always (Verify capabilities are affected-gated automatically, Publish and Deploy ones under zipxAffectedPublish / zipxAffectedDeploy) or Gate.OnReleaseTag.
.github/zipx/action-pins.yml:1: unknown pin 'chekout'; expected one of: checkout, setupJava, setupSbt, setupNode, cache, uploadArtifact, downloadArtifact, extra:
chekout: actions/checkout@v7.0.1
.github/zipx/action-pins.yml:1: pin 'checkout' must name actions/checkout, but this ref is 'actions/setup-java@v5'
checkout: actions/setup-java@v5Generate time: warnings for the escape hatches
Script.raw, Step.runRaw, JobCondition.raw and SbtCommand.raw exist because no AST covers everything. They
are not holes: the text rules that would corrupt the generated file still apply. What they skip is the structure, so
api/tets is a failing job rather than a compile error.
Steps.rawWarnings reports every one at generate time, naming the bundle or capability, because an escape hatch you
cannot see being used is one you cannot review. A warning, not an error: the hatch is legitimate, and zipx does not
get to decide that your sbt syntax is wrong. Reaching for a hatch inside a bare lambda instead of a named
Steps.built(...) bundle hides it from this report, which is the incentive to use the bundle.
SbtCommand.raw("api/tets") match
case Left(error) => s"unexpected rejection: $error"
case Right(cmd) =>
val smoke = Capability.custom(name = CapabilityName("smoke"), command = _ => cmd)
Steps.rawWarnings(List(smoke), DocsFixtures.config).mkString("\n")capability 'smoke' uses an unchecked sbt command: api/tets. zipx validates it as text that cannot corrupt the generated file, but not as sbt syntax, so a typo is a failing job rather than a compile errorWhat is left to the runner
Two things zipx does not check, on purpose:
Whether the sbt command succeeds. Even a
Builtcommand is only known to be well-formed text naming a real module and task. Whetherapi/testpasses is the job's business.Whether a secret exists. zipx handles secret names, never values, so
secret"DEPLOY_ROLE"checks the name's shape and nothing more. A name that is not configured in the repository renders as an empty string on the runner, which GitHub does not treat as an error.
And one rule the whole library follows: nothing in modules/*/src/main throws. Failures are Either, and
ZipxPlugin.orFail is the single seam where one becomes a thrown sbt error, because sbt's task contract is that a task
fails by throwing. A library caller still sees the Either.