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:

ModuleTypes
zipx-shellShText, SquoteText, ParamText, ScriptLine, VarName, GlobPattern, ProgramName, HeredocTag, ExitCode, FileDescriptor
zipx-workflowJobId, StepId, SecretName, EnvName, OutputName, MatrixAxis, ContextPath, ActionRef, EventName, FunctionName, ExprLiteral, RawExpr, CronHour, CronMinute, CronExpr
zipx-coreModuleId, CapabilityName, TargetName, WorkflowName, RunnerOs, JdkVersion, NodeVersion, SbtCommandText
zipx-awsAwsAccountId, 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: true

Typed 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:

CheckWhat it catches
validateCapabilities: Gate.AffectedOnlyan unimplemented seam that would silently behave as Gate.Always
validateCapabilities: needsCapabilities cycletwo capabilities each waiting on the other
validateWorkflowCallcontainer: / services: beside workflowCall, which GitHub rejects
validateSharedTargetsa per-destination condition or environment on a SharedJob capability
validateSatisfiablea gate/condition conjunction that can never be true (see Job conditions)
ModuleGraph.makea dependency cycle, or two modules with one id
ModuleId.make on every sbt project ida project id sbt allows and a GitHub job id does not
Step.validate / YamlPrinter.problem at rendera hand-built step, or content YAML would mangle
Action constructor / leftover pin YAMLa short SHA, a leftover .github/zipx/action-pins.yml, a duplicate Action name
leftover zipx-scala-steward.ymlFail (default) or Warn(reason); the replacement companion is zipx-version-updates.yml
Ship membership / library publish topologya 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@v5

Generate 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 error

What is left to the runner

Two things zipx does not check, on purpose:

  • Whether the sbt command succeeds. Even a Built command is only known to be well-formed text naming a real module and task. Whether api/test passes 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.