You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
promises/inputs labels are a user-facing ordering mechanism. If a user can
write them, they must work everywhere — both across deploys and between
steps of the same deploy. Today they only work across deploys; using a label
to order two steps inside one deploy doesn't order anything and instead crashes
scampi with an "internal error".
[scampi] fatal internal error
This is a BUG in scampi, not in your configuration.
...
BUG: engine.Plan returned unexpected error: deploy block cycle: base/local
Root causes
1. Labels don't reach the intra-deploy step graph. There are two separate
resource paths:
step_graph.go builds the within-deploy step DAG from the step-level spec.Promiser interface (step.Promises()/Inputs()).
deploy_graph.go builds the cross-deploy DAG from the step config's spec.ResourceDeclarer (ResourceDeclarations()), which is where user promises/inputs labels actually land.
So user labels feed only the cross-deploy graph. run steps expose nothing via spec.Promiser, so intra-deploy they're always barriers and labels between them
order nothing.
2. The cross-deploy graph fabricates a self-cycle. Because collectPromises/ collectInputs aggregate over all of a deploy's steps, a deploy whose steps
promise and input the same label both produces and consumes it. The wiring loop
then adds a self-edge (deploy_graph.go:69-77) with no guard:
n.deps=append(n.deps, prods[0]) // no "prods[0] != n" check
The step graph gets this right (step_graph.go:59 skips producer == n); the
deploy graph doesn't. findCycle sees base -> base and returns DeployCycleError.
3. Deploy cycles crash instead of diagnosing.DeployCycleError is a proper
typed diagnostic (it has a Diagnostic() method), but handleEngineError
(main.go:348) only handles AbortError/CancelledError; everything else hits panic(errs.BUG(...)). So a config error prints "This is a BUG in scampi, not
in your configuration" with a stack trace — the opposite of the truth. MultipleProducersError has the same fate.
Desired behavior
Same-deploy labels order steps. A step that inputs a label another step in
the same deploy promises depends on that step (promiser runs first) — via the
intra-deploy step graph. This makes run steps orderable by labels, not just
fenced by barriers.
Cross-deploy graph ignores intra-deploy-satisfied labels. A label produced
and consumed within one deploy is internal; it must not create a cross-deploy
edge or a self-cycle. Add the producer != n guard, mirroring the step graph.
Genuine deploy cycles are diagnostics, not panics. Route DeployCycleError and MultipleProducersError through the diagnostic path in handleEngineError so they render as guiding user errors (and carry a SourceSpan — see engine: MultipleProducersError / DeployCycleError carry no SourceSpan #367), never the internal-bug panic.
Files
internal/engine/step_graph.go — feed user labels into the step DAG.
internal/engine/deploy_graph.go — self-edge guard; treat intra-deploy-satisfied
labels as internal.
cmd/scampi/main.go — handle DeployCycleError/MultipleProducersError as
diagnostics in handleEngineError.
Principle
promises/inputslabels are a user-facing ordering mechanism. If a user canwrite them, they must work everywhere — both across deploys and between
steps of the same deploy. Today they only work across deploys; using a label
to order two steps inside one deploy doesn't order anything and instead crashes
scampi with an "internal error".
Repro
Chain three
runsteps in one deploy via labels:Root causes
1. Labels don't reach the intra-deploy step graph. There are two separate
resource paths:
step_graph.gobuilds the within-deploy step DAG from the step-levelspec.Promiserinterface (step.Promises()/Inputs()).deploy_graph.gobuilds the cross-deploy DAG from the step config'sspec.ResourceDeclarer(ResourceDeclarations()), which is where userpromises/inputslabels actually land.So user labels feed only the cross-deploy graph.
runsteps expose nothing viaspec.Promiser, so intra-deploy they're always barriers and labels between themorder nothing.
2. The cross-deploy graph fabricates a self-cycle. Because
collectPromises/collectInputsaggregate over all of a deploy's steps, a deploy whose stepspromise and input the same label both produces and consumes it. The wiring loop
then adds a self-edge (
deploy_graph.go:69-77) with no guard:The step graph gets this right (
step_graph.go:59skipsproducer == n); thedeploy graph doesn't.
findCycleseesbase -> baseand returnsDeployCycleError.3. Deploy cycles crash instead of diagnosing.
DeployCycleErroris a propertyped diagnostic (it has a
Diagnostic()method), buthandleEngineError(
main.go:348) only handlesAbortError/CancelledError; everything else hitspanic(errs.BUG(...)). So a config error prints "This is a BUG in scampi, notin your configuration" with a stack trace — the opposite of the truth.
MultipleProducersErrorhas the same fate.Desired behavior
the same deploy promises depends on that step (promiser runs first) — via the
intra-deploy step graph. This makes
runsteps orderable by labels, not justfenced by barriers.
and consumed within one deploy is internal; it must not create a cross-deploy
edge or a self-cycle. Add the
producer != nguard, mirroring the step graph.DeployCycleErrorandMultipleProducersErrorthrough the diagnostic path inhandleEngineErrorso they render as guiding user errors (and carry aSourceSpan— see engine: MultipleProducersError / DeployCycleError carry no SourceSpan #367), never the internal-bug panic.Files
internal/engine/step_graph.go— feed user labels into the step DAG.internal/engine/deploy_graph.go— self-edge guard; treat intra-deploy-satisfiedlabels as internal.
cmd/scampi/main.go— handleDeployCycleError/MultipleProducersErrorasdiagnostics in
handleEngineError.Discovered while building the multi-deploy live-region demo: chaining
runsleeps within a lane via labels crashed rather than ordering them.