Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions candy/plugin-check/bed_build_window_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package check

import (
"testing"

"github.com/opencharly/spec/poll"
)

// TestBuildStepEnvSizesTheChildGuardWindow pins the #123 fix: the generated BUILD steps
// carry a build-sized no_progress window to the child through the cli seam's per-call env,
// and that window RESOLVES with the child's other readiness defaults — an override the
// child rejects would silently fall back to the 90s probe window, which is the defect
// itself (the child's idle guard then reads an IO-bound `podman load`/cold export as a
// wedged call and SIGKILLs it).
func TestBuildStepEnvSizesTheChildGuardWindow(t *testing.T) {
env := buildStepEnv()
got := env["CHARLY_READINESS_NO_PROGRESS"]
if got != buildStepNoProgress {
t.Fatalf("buildStepEnv()[CHARLY_READINESS_NO_PROGRESS] = %q, want %q", got, buildStepNoProgress)
}

// The env name and value must be exactly what the child's resolver reads, and the child
// must not reject the override (ordering: every interval <= no_progress <= absolute_cap).
t.Setenv("CHARLY_READINESS_NO_PROGRESS", got)
rr, err := poll.ResolveReadiness(nil)
if err != nil {
t.Fatalf("the build step's window does not resolve with the child's readiness defaults: %v", err)
}
if rr.NoProgress <= poll.ReadinessNoProgressFallback {
t.Fatalf("the build step's window resolves to %s, which does not exceed the probe fallback %s",
rr.NoProgress, poll.ReadinessNoProgressFallback)
}
}
29 changes: 26 additions & 3 deletions candy/plugin-check/bed_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,24 @@ func bedRunGuard(cap *bedStderrCapture, dir, name string, state *bedRunState, re
}
}

// buildStepNoProgress is the CHILD's readiness no_progress window for a generated BUILD
// step. The image-build step runs `charly box build` through the cli seam, and the child's
// own plugin dispatch carries an idle guard whose window is the project's readiness
// no_progress (default 90s). A build's IO-bound phases — the OCI bridge's `podman load`, a
// cold layer export — make no host-visible progress for minutes, which the guard reads as a
// wedged call and SIGKILLs (plugin-check#123, measured live as
// `bridging … into the podman store: podman load …: signal: killed` at 2m2s). The harness
// must not be the reason a legitimate image build fails, so the build steps size the child's
// window through the seam's per-call env — a build-sized bound, never the probe floor. 30m
// stays within the default absolute_cap (30m), so the child's readiness ordering validates
// without a second override.
const buildStepNoProgress = "30m"

// buildStepEnv is the per-call env that carries buildStepNoProgress to a build step's child.
func buildStepEnv() map[string]string {
return map[string]string{"CHARLY_READINESS_NO_PROGRESS": buildStepNoProgress}
}

// runCheckBed executes the canonical R10 sequence for one check bed and writes
// per-step logs + summary.yml to .check/<name>/<calver>/. Returns the result struct
// (always non-nil once setup succeeds) and the first error encountered.
Expand Down Expand Up @@ -795,7 +813,7 @@ func runCheckBed(ctx context.Context, ex *sdk.Executor, name string, opts bedRun

// step records a step's outcome (a `charly` subcommand over the cli seam) and
// writes its log file. Returns the run error so the caller can short-circuit.
step := func(stepName string, argv ...string) error {
stepEnv := func(stepName string, env map[string]string, argv ...string) error {
state.enterStep(stepName)
defer state.leaveStep()
t0 := time.Now()
Expand All @@ -805,7 +823,7 @@ func runCheckBed(ctx context.Context, ex *sdk.Executor, name string, opts bedRun
if writeErr := os.WriteFile(logPath, []byte("status: RUNNING\ncommand: "+command+"\n"), 0o644); writeErr != nil {
fmt.Fprintf(os.Stderr, "charly check run %s: [%s] cannot initialize log %s: %v\n", name, stepName, logPath, writeErr)
}
reply, cerr := bedCliCombined(ex, ctx, argv...)
reply, cerr := bedCliCombinedEnv(ex, ctx, env, argv...)
dur := time.Since(t0)
logText := cliStepLog(reply)
// R10's "a warning is not a pass", MEASURED. Scan the retained log before deciding the
Expand Down Expand Up @@ -873,6 +891,11 @@ func runCheckBed(ctx context.Context, ex *sdk.Executor, name string, opts bedRun
return nil
}

// step runs a non-build step: its child keeps the project's readiness windows.
step := func(stepName string, argv ...string) error {
return stepEnv(stepName, nil, argv...)
}

// cleanup is the ONE venue-teardown BODY: nested children in reverse deploy order, then the
// root verb, then deploykit.TearDownMembers. It is called ONLY by bedVenueTeardown, which owns
// the gate (retention / disposability / already-torn-down) — the body therefore carries no
Expand Down Expand Up @@ -1014,7 +1037,7 @@ func runCheckBed(ctx context.Context, ex *sdk.Executor, name string, opts bedRun
if bedBuildsImage(d.IsVM, isKubeVirt, d.IsLocal, d.Image) {
// Disposable check beds ALWAYS bake the IN-DEVELOPMENT charly toolchain via
// --dev-local-pkg — so a bed tests the code under development.
if err := step("image-build", withRunTag([]string{"box", "build", d.Image, "--dev-local-pkg"}, d.ImageTag)...); err != nil {
if err := stepEnv("image-build", buildStepEnv(), withRunTag([]string{"box", "build", d.Image, "--dev-local-pkg"}, d.ImageTag)...); err != nil {
return fail("image build %s: %w", d.Image, err)
}
if d.RunBuild {
Expand Down
17 changes: 10 additions & 7 deletions candy/plugin-check/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,18 @@ func bedCli(ex *sdk.Executor, ctx context.Context, capture bool, argv ...string)
return bedCliReq(ex, ctx, spec.CliRequest{Argv: argv, Capture: capture})
}

// bedCliCombined is bedCli with COMBINED capture (stdout+stderr merged into reply.Stdout) — used for
// the check-bed per-step .log so a `charly check …` child's STDERR-written results are persisted
// (pre-relocation parity: core runCapture captured combined output; plain bedCli captures stdout
// only, which would drop the check results from the log).
func bedCliCombined(ex *sdk.Executor, ctx context.Context, argv ...string) (spec.CliReply, error) {
return bedCliReq(ex, ctx, spec.CliRequest{Argv: argv, Capture: true, Combined: true})
// bedCliCombinedEnv is the check-bed per-step cli call: COMBINED capture (stdout+stderr merged
// into reply.Stdout, so a `charly check …` child's STDERR-written results are persisted — plain
// bedCli captures stdout only, which would drop the check results from the log) plus a per-call
// ENV — the seam's placement-invariant carrier for settings the CHILD must resolve for itself
// (its own readiness bounds), which the caller cannot set into a shared process. The generated
// BUILD steps use it to size the child's idle window (buildStepNoProgress, bed_run.go) so an
// IO-bound image build is not read as a wedged plugin call (plugin-check#123).
func bedCliCombinedEnv(ex *sdk.Executor, ctx context.Context, env map[string]string, argv ...string) (spec.CliReply, error) {
return bedCliReq(ex, ctx, spec.CliRequest{Argv: argv, Capture: true, Combined: true, Env: env})
}

// bedCliReq is the shared cli-seam marshal/dispatch/decode (R3 — one body for bedCli/bedCliCombined).
// bedCliReq is the shared cli-seam marshal/dispatch/decode (R3 — one body for bedCli/bedCliCombinedEnv).
//
// The forked child gets the invocation's per-bed env explicitly via CliRequest.Env (never via
// inherited process env): the bed run carries its RunEnv on ctx (spec.RunEnvFrom), and this
Expand Down
Loading