From e4da83496f2da3bd5e5c9e1a30cfacaf3c2eada7 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 23:31:06 +0200 Subject: [PATCH] A run-once step may name what it reads: the pair run-once + restart-on is no longer refused (novox/hq ADR 0099) --- internal/catalogue/manifest.go | 18 +++++------------- internal/catalogue/runonce_test.go | 15 +++++++-------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/internal/catalogue/manifest.go b/internal/catalogue/manifest.go index 9f37ebc..601c647 100644 --- a/internal/catalogue/manifest.go +++ b/internal/catalogue/manifest.go @@ -985,11 +985,11 @@ func ParseManifest(raw []byte) (Manifest, error) { } // **A run-once container is a step the host runs to completion** (novox/hq ADR 0052). It is a // boolean modifier on the container shape — the host runs the container, requires it to exit 0, - // and starts whatever the declaration places after it only once it has. Two things are refused - // here rather than only on the machine, for the same near-versus-far reason the action ban - // above records: a value that is not a boolean, and the pair run-once + restart-on, which asks - // for two contradictory lifecycles — restart-on brings a *running* container back, and a - // run-once step does not stay running. + // and starts whatever the declaration places after it only once it has. A value that is not a + // boolean is refused here rather than only on the machine, for the same near-versus-far reason + // the action ban above records. A run-once step may name what it reads under restart-on: for a + // step the word means *run again* when one of those changed, which is how a fact fetched from a + // provider is fetched again when the provider moved (novox/hq ADR 0099). for _, r := range m.Resources { if fmt.Sprint(r["type"]) != "container" { continue @@ -1003,14 +1003,6 @@ func ParseManifest(raw []byte) (Manifest, error) { m.Module, r["id"], raw)) } else { runOnce = once - if once { - if _, hasRestart := r["restart-on"]; hasRestart { - problems = append(problems, fmt.Sprintf( - "%s declares %v as run-once and with restart-on; a run-once step runs to "+ - "completion rather than staying running to be restarted (novox/hq ADR 0052)", - m.Module, r["id"])) - } - } } } // **A scheduled container runs on a recurring cadence** (novox/hq ADR 0053), the recurring diff --git a/internal/catalogue/runonce_test.go b/internal/catalogue/runonce_test.go index e7c9fb4..9b17a58 100644 --- a/internal/catalogue/runonce_test.go +++ b/internal/catalogue/runonce_test.go @@ -77,17 +77,16 @@ func TestARunOnceMustBeABoolean(t *testing.T) { } } -func TestARunOnceContainerCannotAlsoDeclareRestartOn(t *testing.T) { - // restart-on brings a running container back; a run-once step does not stay running. The pair - // is a contradiction, refused at the manifest rather than surfacing far away on the host. +func TestARunOnceStepMayNameWhatItReads(t *testing.T) { + // For a step, restart-on means *run again* when what it reads changed: a gate that fetches a + // provider's root names the binding file it reads, so a provider that moved is fetched again + // (novox/hq ADR 0099). Accepted here, and the host's digest does the rest. digest := "@sha256:" + strings.Repeat("a", 64) - bad := []byte(`{"module":"m","resources":[ + good := []byte(`{"module":"m","resources":[ {"id":"conf","type":"file","path":"/x","content":"y"}, {"id":"seed","type":"container","name":"seed","image":"registry.example/x` + digest + `","run-once":true,"restart-on":["conf"]} ]}`) - if _, err := ParseManifest(bad); err == nil { - t.Error("a run-once container that also declared restart-on was accepted") - } else if !strings.Contains(err.Error(), "restart-on") { - t.Errorf("refused for the wrong reason: %v", err) + if _, err := ParseManifest(good); err != nil { + t.Errorf("a run-once step naming what it reads was refused: %v", err) } }