Commit Graph
5994 Commits
Author SHA1 Message Date
Guillaume Lours f729794270 docs(hooks): document pre_start hook container retention and cleanup
- compose_down.md: note that retained pre_start hook containers (those
  kept after a hook failure for post-mortem) are automatically removed
  by compose down, with the label filter to list them beforehand.
- compose_up.md: note the retention behavior on pre_start hook failure,
  how to inspect the retained container, and that it is cleaned up on
  the next compose up or compose down.

Signed-off-by: Guillaume Lours <[email protected]>
2026-08-20 18:57:02 +02:00
Guillaume Lours 54785c4be6 feat(hooks): clean up retained pre_start containers on compose down
A pre_start hook container retained after failure (introduced in the
previous commit) is created without ConfigHashLabel so getContainers()
and every downstream compose operation (down, ps, stop, rm) is blind
to it. Without explicit cleanup it would survive compose down.

This commit adds removePreStartHookContainers(), called from down()
after regular container teardown and before network removal (so hook
containers are gone before their networks are removed). The cleanup is
project-wide on a full compose down; scoped to the requested services
when compose down <svc...> is used. Individual ContainerRemove errors
are logged at warn level and do not abort the teardown.

Changes:
- pkg/compose/down.go: removePreStartHookContainers helper + call in down()
- pkg/compose/filters.go: add //nolint:unparam to hookFilter (param is
  architecturally correct; today only preStartHookType is in use)
- pkg/compose/down_test.go: update all existing down tests to expect the
  new ContainerList call; add TestDownRemovesRetainedPreStartHookContainers
  and TestDownHookContainerRemovalFailureIsNonFatal

Signed-off-by: Guillaume Lours <[email protected]>
2026-08-20 18:57:02 +02:00
Guillaume Lours 674010661a feat(hooks): retain failed pre_start containers for post-mortem
A failing pre_start hook container is now retained (AutoRemove: false)
so operators can run 'docker logs <id>' and 'docker ps -a' to diagnose
the failure. On success the container is removed explicitly, mirroring
the old AutoRemove behaviour including anonymous volumes.

Before each pre_start run, stale hook containers from a previous failed
run are detected via project+service+HookLabel filters and force-removed
so they do not accumulate.

Changes:
- pkg/api/labels.go: add HookLabel (com.docker.compose.hook)
- pkg/compose/filters.go: add hookFilter helper
- pkg/compose/pre_start.go:
  - AutoRemove: false in createPreStartContainer
  - HookLabel added to container labels
  - runPreStartHook: explicit ContainerRemove on success; retain on failure
  - removeOrphanPreStartContainers: new helper called in runPreStart
- Tests: update all existing pre_start tests for the new flow; add
  feature tests (success removes, failure retains, orphan cleanup) and
  coverage-gap tests (lowestNumberedContainer, waitPreStart cancel,
  preStartResultErr, streamPreStartLogs error paths, old-API network
  paths, ExecCreate/Attach/Inspect errors, hookExitError branches)

Coverage after: hook.go 100%, pre_start.go most functions 100%
(was 89% and 64% respectively per Codecov delta).

Signed-off-by: Guillaume Lours <[email protected]>
2026-08-20 18:57:02 +02:00
Guillaume Lours bbec45855a fix(hooks): stderr-biased tail and pre_start parity
On top of the output-tail infrastructure from the previous commit:

Stderr bias: split the combined output tail into separate stdout and stderr
buffers. stdcopy.StdCopy(wOut, wErr, reader) now routes each stream to its
own outputTail. The error message prefers stderr (where hooks write their
actual error) and falls back to stdout when stderr is empty:

  db hook exited with status 1: Table 'service.sites' doesn't exist

A new hookExitError helper encapsulates this preference.

pre_start parity: streamPreStartLogs previously short-circuited and returned
immediately when listener was nil (detached mode / compose up -d), leaving
the tail buffers empty and failure errors without context. The log stream is
now always opened. The demultiplexed streams fill separate tailOut / tailErr
buffers. On non-zero exit the stderr-biased tail is appended to the error
(same behaviour as exec hooks).

Tests:
- TestRunHook_StderrBias: stdout noise stays out of the error when stderr
  has content.
- TestPreStart_DetachedModeAttachesLogs: ContainerLogs is called even with
  a nil listener.
- TestPreStart_FailureIncludesTail: a failing pre_start hook carries its
  stderr output in the returned error.

Signed-off-by: Guillaume Lours <[email protected]>
2026-08-20 18:57:02 +02:00
Nicolas De Loof b857135cb9 fix: hold startMx on the start path actually exercised
startMx exists to serialize ContainerStart calls because the engine
assigns published ports from ranges non-atomically. It is held by the
plan executor's execStartContainer — a nearly dead code path (plans
only emit OpStartContainer for paused/dead containers) — and NOT by
startServiceContainer, the path every up/start actually runs. The
protection is thus inoperative today: concurrent services publishing
overlapping port ranges can race in the engine.

Take the mutex around the real ContainerStart call and document the
contract on the variable. Starts of different services were already
serialized per-service (replicas start sequentially); this serializes
the API call across services too, matching what the plan engine will
do once the start phase converges (#14081).

Part of #14074 (section C) / #14081 (lot 0).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 15:30:03 +02:00
Nicolas De Loof 06f4f9d622 fix: dependency wait timeout surfaced only by luck
waitDependency returned nil whenever its context ended, so an expired
wait deadline was reported only when the 500ms poll ticker happened to
win the select race and the condition check then failed on the expired
context. Whether `up --wait --wait-timeout` failed or silently
succeeded after the timeout was a coin toss, and the "timeout waiting
for dependencies" mapping in waitDependencies was unreachable on the
direct path.

Return the context error when the deadline expired; a plain
cancellation (Ctrl-C) is still not reported as a dependency failure.
The unit test pins both sides deterministically (a deadline shorter
than the first tick used to be swallowed with certainty); the e2e
scenario documents the user-visible contract: `up --wait
--wait-timeout` on a never-healthy service fails with "application not
healthy after Xs" while the container keeps running.

Part of #14074 (section C) / #14081 (lot 0).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 15:16:57 +02:00
Nicolas De Loof 899d040f3f ci: validate the generated mocks are in sync
`pkg/mocks` was only kept in sync by discipline: nothing failed when an
interface in `pkg/api` changed without regenerating. Add a
`mocks-validate` Dockerfile stage on the vendor-validate model, wire it
as a bake target, a `make validate-mocks` shortcut and a new entry in
the CI validate matrix.

Part of #14074 (section G).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 15:16:43 +02:00
Nicolas De Loof 6443bc1da2 chore(mocks): fix the stale mockgen invocation and pin its version
The `mocks` target still passed the pre-rename `Service` argument to
mockgen (the interface is `Compose` since the rename) and installed
whatever mockgen happened to be on PATH. Run it through `go run` with a
pinned version so regeneration is reproducible; v0.4.0 is the last
version usable here, as the package mode introduced in v0.5 copies
parameter names from the mocked source and moby's APIClient names its
variadic options `_`, which produces invalid Go code.

Regenerated pkg/mocks with the fixed invocation (header-only change).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 15:16:43 +02:00
Nicolas De Loof cccb9f1f7e cli: display.Mode always resolves to the mode actually rendered
The display.Mode package global was left as "auto" on the most common
path (auto mode with an interactive or redirected terminal), so readers
like `up`'s NavigationMenu gate saw an unresolved value; that gate also
compared against the string literal "plain" instead of the constant.
applyDisplayMode wrote a mode that selectEventProcessor could then
contradict (--ansi always with a redirected stderr rendered Plain while
Mode said tty).

selectEventProcessor becomes the single resolution point: every branch
assigns the mode matching the renderer it returns, so after command
setup the global never holds ModeAuto. applyDisplayMode loses its
redundant writes and its misleading name (now applyAnsiMode); the
contract is documented on the Mode variable, and the existing
selectEventProcessor tests now pin the resolved mode for every branch.

Side effect, deliberate: with stderr redirected (auto -> plain
rendering) the navigation menu is no longer offered; it used to slip
through because Mode still read "auto".

Part of #14074 (section E).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 15:10:15 +02:00
Nicolas De Loof 0d7d22585a test: unit-lock the imperative start path
The imperative start engine (startService, startServiceContainer,
restart, waitDependencies) is only locked by e2e tests today. Before
converging it into the plan engine (#14081), pin its observable
behavior with unit characterization tests: start idempotence and the
silent no-op on running replicas, the "no container to start" error,
pre_start gating (mixed replicas skip the hook, all-stopped runs it
once on the lowest-numbered replica), the inject -> start -> post_start
sequence with Started only emitted after hooks, restart's depends_on
restart:true selection and its pre_stop -> ContainerRestart ->
post_start order, --wait's completed_successfully condition mapping,
and waitDependencies' missing/failing dependency semantics for
required and optional dependencies.

No production change.

Part of #14074 (section C) / #14081 (lot 0).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 15:09:16 +02:00
Guillaume Lours 3a62ae63ce fix(ps): honor psFormat from Docker CLI config when no --format flag given
The --format flag defaulted to "table" instead of "", so the
opts.Format == "" guard in runPs never fired for a bare compose ps.
Setting the default to "" lets the existing config-fallback path work,
matching the behaviour of docker ps.

Fixes #13643
Supersedes #14032

Signed-off-by: Guillaume Lours <[email protected]>
2026-08-20 14:05:34 +02:00
dependabot[bot] c594d34f00 build(deps): bump github/codeql-action/upload-sarif
Bumps [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) from 4.37.6 to 4.37.7.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/5595ccaf912efad79be6eef63a5619ff05969be3...ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd)

---
updated-dependencies:
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.37.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-20 12:38:22 +02:00
Guillaume Lours 9e4a3c7457 fix(network): pass IPAM options when creating networks
createNetwork built the IPAM object in two separate blocks; the second
silently overwrote createOpts.IPAM with a fresh empty struct and never
forwarded n.Ipam.Options to the daemon. Consolidate into a single block
covering driver, config pool, and options.

Fixes #13785
Supersedes #13936

Signed-off-by: Guillaume Lours <[email protected]>
2026-08-20 12:36:23 +02:00
Nicolas De Loof 5671532583 api: RunOptions documents its partial embedding and its double life as ExecOptions
RunOptions embeds CreateOptions, which suggests every creation option
applies to the one-off's dependencies; in reality only Build,
IgnoreOrphans, RemoveOrphans and QuietPull are propagated
(startDependencies builds a fresh CreateOptions from exactly those
four). Setting Recreate or Inherit on a RunOptions was a silent no-op.
The same struct also serves Exec, which honors only the exec-relevant
subset of its ~20 fields.

Both facts are now stated on the type, the embedding and the
Exec-only Index field.

Part of #14074 (B: pkg/api promises things the implementation does not
honor).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 12:32:25 +02:00
Nicolas De Loof 62eb97e8e5 api: StartOptions says which half of it Start actually reads
StartOptions fuses two contracts: the fields s.start honors (Project,
AttachTo-as-project-source, Wait, WaitTimeout) and the fields only Up's
foreground session reads (Attach, OnExit, ExitCodeFrom, Watch,
NavigationMenu). Passing the latter to Start compiled, returned no
error, and did nothing.

Each session field is now marked 'honored by Up's foreground session
only; ignored by Start'. Two implicit behaviors become explicit along
the way: Attach doubles as the detached/interactive mode switch (nil
means Up returns once containers are started), and AttachTo has two
unrelated meanings (project reconstruction source for Start, log
scoping for Up).

Part of #14074 (B: pkg/api promises things the implementation does not
honor).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 12:32:25 +02:00
Nicolas De Loof 00ce78a055 api: state the scoping invariant Services fields never had
Three structs carry a Services field with three different meanings:
ProjectLoadOptions.Services narrows the loaded project (the only real
filter), CreateOptions.Services only selects which recreation policy
applies to which service while the WHOLE project converges, and
StartOptions.Services only scopes Up's log monitor and is ignored by
Start. Nothing said so: an SDK consumer calling
Create(project, CreateOptions{Services: []string{"web"}}) reasonably
expected to create web alone, and got the full project.

The interface doc now states the underlying invariant — methods act on
the whole project they receive; scoping is done by narrowing the
project, and downstream Services fields are intent markers, not
filters — and each of the three fields documents its actual effect.

Part of #14074 (B: pkg/api promises things the implementation does not
honor).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 12:32:25 +02:00
Guillaume Lours ea8d9bc3db fix(watch): sync all files on initial_sync, regardless of mtime
initialSyncFiles and initialSyncDirectory filtered out any host file
whose mtime predated the image creation time — which is exactly the
files that initial_sync is meant to copy. The imageCreatedTime lookup
(ContainerList + ImageInspect) was also a silent failure point when the
container list returned nothing. Remove the filter and the helper
entirely: sync all files that are not ignored or bind-mounted.

Fixes #13725
Supersedes #14031

Signed-off-by: Guillaume Lours <[email protected]>
2026-08-20 12:05:36 +02:00
Guillaume Lours e773a0789e docs: update CONTRIBUTING.md to reflect current tooling and practices
Signed-off-by: Guillaume Lours <[email protected]>
2026-08-20 12:04:50 +02:00
Nicolas De Loof 24059c688c chore: explicit, unique, greppable names for internal identifiers
Rename-only change, no behavior difference, no exported identifier
renamed (only parameter names of a few exported functions, which are
not part of the Go API).

- names that hide what a value is: parameters holding a *name* or *ID*
  while named like the object — project→projectName, service→serviceName,
  container→containerID — so string vs struct is readable at every call
  site without opening the signature
- misleading receivers: monitor methods used c (reads as
  client/container/CLI) → m; jsonWriter used p while its ttyWriter
  sibling uses w → w
- cryptic abbreviations with non-trivial scope: nw (meant both
  network.Summary and types.NetworkConfig) → network/networkConfig,
  svc → service/serviceName/serviceCopy, oc/ocs → observedContainer(s),
  cnts → serviceContainers, cnx → attachResponse
- container.Summary loop variables unified on ctr (was a c/ctr mix);
  fillBindMounts also dropped its p/s/m single-letters, one of which
  was shadowed by a loop variable of a different type
- option-struct identifiers unified on options (opts remains only for
  variadic functional options); ambiguous ones get a precise name
  (bindOptions, networkCreateOptions, projectOptionsFns)

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 11:17:11 +02:00
Nicolas De Loof 3ee4b5c793 api: ScaleOptions carries the replica counts it always pretended to
The Compose interface promised 'Scale manages numbers of container
instances running per service', but ScaleOptions had no replica count:
callers had to know they must mutate project.Services[x] via SetScale
before calling, and ScaleOptions.Services only tuned the recreation
policy. An SDK consumer following the interface doc got a no-op
convergence of the whole project.

ScaleOptions gains Replicas (service -> count); the backend applies it
to the model itself before converging, and derives the targeted
services from its keys when Services is not set. The CLI now does what
a CLI should: parse SERVICE=REPLICAS tuples and hand them to the
backend, instead of pre-mutating the model. Passing a pre-mutated
project with an empty Replicas map still behaves as before, so
existing SDK callers are unaffected.

Part of #14074 (B: pkg/api promises things the implementation does not
honor).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 10:16:06 +02:00
Nicolas De LoofandClaude Fable 5 9a70c817fa fix(build): TTY progress on Windows — hand the real stdout to buildkit
The bake progress display goes through containerd/console, which on
Windows only accepts the exact os.Stdin/Stdout/Stderr values (pointer
identity in newMaster) and rejects everything else with "creating a
console from a file is not supported on windows". The _console wrapper
introduced to satisfy buildkit's console.File type-assert therefore
could never pass that check: TTY progress silently fell back to plain
in auto mode, and --progress=tty / --ansi always failed hard with
"failed to get console".

When the CLI stream was constructed from a real file — the interactive
case, where it wraps os.Stdout — hand that genuine *os.File to
NewDisplay so the identity check passes. File-less streams keep the
wrapper, which works on Unix where only the descriptor matters.

Fixes #14086

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-19 18:06:55 +02:00
Guillaume Lours 8bd79da4bc e2e: fix test resource isolation and make standalone parallelism configurable
Several e2e tests used fixed host ports (8070, 8080, 8090, etc.), global
buildx builders registered with --use, and a hardcoded IPC container name.
Under parallel execution these shared daemon-level resources caused port
conflicts, buildkitd container-name collisions, and intermittent failures.
Separately, the standalone make target hardcoded -parallel=1, forcing the
entire standalone suite to run serially even though the same tests run in
parallel in plugin mode.

Changes:
- Convert fixed host ports in four fixtures (network-test, sentences,
  build-test, volume-test) to ephemeral bindings; add ServicePublishedPort
  helper to framework.go to resolve the actual mapped port at runtime.
- Give each buildx-builder test a unique daemon-scoped name via BuilderName
  helper, preventing container-name collisions between parallel goroutines.
  Applies to TestBuildPlatformsWithCorrectBuildxConfig, TestBuildPrivileged,
  TestBuildBuilder, TestBuildEntitlements, TestBuildTLS.
- Rename the fixed ipc_mode_container to a project-scoped name
  (ipc_e2e-src) and update the ipc-test fixture accordingly.
- Replace the hardcoded -parallel=1 in make e2e-compose-standalone with
  E2E_PARALLEL_PLUGIN?=4 and E2E_STANDALONE_PARALLEL?=4, aligning both
  modes with the effective GOMAXPROCS on 4-core GitHub Actions runners.
  Both variables are overridable (E2E_STANDALONE_PARALLEL=1 restores
  serial local debugging).
- Raise the poll delay in watch_test.go long-running polls (90s/120s
  timeout) from the default 100ms to 1s; bump cascade_test.go and
  publish_test.go from 100ms to 500ms. Reduces CPU pressure without
  affecting effective test duration.

The logging-driver fixture retains its fixed port 24224: the Docker fluentd
log driver encodes fluentd-address at service-start time, making ephemeral
resolution non-trivial. TestLoggingDriver is serial so there is no
collision risk in practice.

Signed-off-by: Guillaume Lours <[email protected]>
2026-08-19 17:41:02 +02:00
Deniss Solnce f8ed846e4d fix(hooks): include hook output in the error when a hook fails
A failing service hook returns only its exit code:

    dependency failed to start: db hook exited with status 1

The output that explains why is not there, and for a hook running without a
listener it does not exist anywhere: runHook created the exec with
AttachStdout/AttachStderr false, so the daemon discarded both streams and
runWaitExec only polled for the exit code. There was nothing left to look at
afterwards.

Now stdout and stderr are always attached, and the last lines of them are kept
in a bounded buffer. On non-zero exit that tail goes into the error:

    db hook exited with status 1: SQLSTATE[42S02]: Base table or view not found

A successful hook is unchanged - output goes to the listener as before, nothing
is added anywhere. With a listener the lines still stream out, the buffer just
tees them.

The buffer keeps the last 10 lines and 2 KiB, drops the oldest content and never
short-writes, so a chatty hook cannot block on it and cannot grow it.

runWaitExec is removed: it existed only for the unattached branch, and that
branch is what threw the output away.

Signed-off-by: Deniss Solnce <[email protected]>
2026-08-19 15:08:25 +02:00
Nicolas De Loof 8b654eba78 docs: document engine container event sequences in up monitor
Map, per container lifecycle scenario, the exact event sequences the
engine emits and the monitor may receive: natural exit, policy-driven
restart (die/start only, no restart action, silence during backoff),
external stop of a running vs restarting container, ContainerRestart,
docker kill, OOM kill, rm -f and rm.

Mapped from moby's daemon sources (LogContainerEvent call sites and
handleContainerExit/containerStop/containerRestart/kill paths) and
verified against a live engine v29.6.1 with 'docker events'. Notably:
the relative order of stop and die on a container stop is not
guaranteed, and no event at all is emitted while a container sits in
restart backoff.

Related to #13985 / #13990.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-19 13:46:30 +02:00
Endika Iglesias f519180b95 fix(watch): sync into a symlinked directory instead of failing
Signed-off-by: Endika Iglesias <[email protected]>
2026-08-18 17:25:13 +02:00
Nicolas De Loof 115898dc13 test(e2e): address review — project-name collisions, file modes, regex compilation
- projectNameFor maps the subtest separator to a double dash so
  TestFoo/bar and TestFooBar cannot resolve to the same compose project
  and interfere when running in parallel
- copyDir preserves the source file mode instead of forcing 0644, so an
  executable testdata script stays executable in the scenario's project
  directory
- OutputMatches and OutputMatchesCount compile their pattern once at
  construction: a malformed pattern panics where the check is declared,
  and Eventually loops no longer recompile it on every tick

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof aef01ae584 test(e2e): batch snapshot inspect with per-container fallback, explicit suite timeout
The per-container inspect of the previous fix doubled the suite's
wall-clock on the containerd CI job — one docker CLI process per
container per snapshot — pushing it past go test's default 10m and
killing the run mid-flight. Snapshots go back to one batch inspect,
falling back to individual inspects only when the batch fails because
a helper container vanished. The e2e make targets also pass an
explicit -timeout 20m so a slow job fails on its own results, not on
the default timeout.

TestUpStopWithLogsMixed becomes deterministic: with a bounded ping the
service could exit on its own before the abort reached it and the
pre_stop hook never ran; it now pings forever so the abort always
interrupts it.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 880c5a2f1d test(e2e): snapshot must survive vanishing helper containers
The plugin/stable/containerd CI job caught a race the DSL had: hook
helper containers carry the project and service labels but no replica
number, and can be destroyed between the snapshot's ps listing and its
batch inspect — which then failed and blanked the entire snapshot,
making ServiceState report 'service has no container' while the
container was right there. Containers are now inspected one by one,
tolerating disappearances, and helper containers (neither numbered as
a replica nor one-off) are excluded from snapshots altogether so they
can't transiently pollute service checks either.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 1fa9ae160a test(e2e): image-identity, images and model scenarios
- TestUpIdempotentContainerdStore joins the scenario style of its
  corner-case siblings: Serial + ContainerdImageStore requirement, the
  regression story in a comment, NotRecreated as the invariant. The
  requireContainerdStore helper and the last identity fixture go away.
- TestImagesAfterImageRemoved scopes its throwaway tag to the project
  and observes container state before pulling the image record away.
- TestComposeModel inlines its model; verified in CI where the
  docker-model plugin is installed (the local runner cannot pull the
  model, as with the legacy test).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 447157274e test(e2e): build scenarios — the 'Built' family inlines its models
TestBuildDependsOn, Subset, DependentImage, SubDependencies,
LongOutputLine, DependentImageWithProfile, Escaped, Tags and Secrets
become scenarios: models inline (the long-line Dockerfile generated
with strings.Repeat instead of 34 copy-pasted lines), image names
project-scoped with Defer cleanup, and the secrets build asserts
ImageExists on top of the Dockerfile's own diff checks.

Kept legacy: TestLocalComposeBuild (DOCKER_BUILDKIT=0/1 matrix over a
shared fixture with fixed names and host port), the buildx-builder
family (SSH, platforms, privileged, entitlements, builder, TLS) which
provisions builders and dind daemons, and TestBuildImageDependencies
(classic/bake matrix). Their fixtures under build-test/ stay; the
single-purpose subdirectories are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof a7293130c6 test(e2e): compose command scenarios — rm, compatibility, config, orphan scoping
- TestRm, TestRemoveOrphaned and TestUnnecessaryResources observe which
  containers exist instead of grepping docker ps.
- TestCompatibility drops the heavyweight sentences fixture: underscore
  naming shows on any model.
- TestConfig/TestConfigInterpolate build their expected rendering from
  the scenario's own project name and directory; OutputMatches joins
  the vocabulary for TestInitContainer's ordering assertion.
- TestStopWithDependenciesAttached inlines its model.
- Kept legacy: TestLocalComposeUp (fixed port, HTTP, shared sentences
  fixture), TestDownComposefileInParentFolder (project name inferred
  from a fixture-relative directory), TestAttachRestart (long-running
  attached up), the COMPOSE_FILE-from-.env family and TestNestedDotEnv
  (they exercise invocation without -f, which ComposeCmd always sets).
- Fixtures init-container/, external/ and dependencies/ are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 37f57744de test(e2e): exec and provider scenarios
- TestExec locks the container-selection order (service container over
  one-off) with one-off state observed at each step.
- providers: a shared providerScenario helper wires the
  example-provider binary into PATH; the env assertions anchor on the
  'test-1  | ' log prefix, replacing the hand-rolled env parser, and
  the stop-hook sentinel file is observed with FileExists.
- Fixtures exec/ and providers/ are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof f4f6ac5324 test(e2e): volume and build-once scenarios
- volumes: external-volume switching, volume recreation on definition
  change and image volumes become scenarios; the two-compose-file
  fixtures collapse into one model with an interpolated volume name or
  label, so the changed definition is visible in the test. The rebuild
  scenario locks the Recreated expectation and scopes the source image
  to the project. TestLocalComposeVolume (fixed host port, HTTP) and
  TestProjectVolumeBind (host file written mid-test) stay legacy.
- build-once: the three run --build regression cases each get a
  scenario; OutputMatchesCount joins the vocabulary to count the build
  reports, replacing the hand-rolled regex counter, and image names are
  project-scoped instead of random suffixes.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof ee7f323fed test(e2e): run and up scenarios
- compose run splits into seven scenarios (one-off basics, ports, deps,
  optional deps, quiet pull, --pull always, chained build contexts).
  The orphan-warning cases fold into the basic scenario: an exited
  one-off IS the orphan the next run warns about, no second compose
  file needed. Piped-stdin and stop-signal tests stay legacy.
- up/compose_up: unhealthy service, dependency exit (both now also
  locking that dependents stay in created state), build dependencies
  (project-scoped image name instead of the fixture's global one),
  optional dependency, --all-resources, profile targeting (dropping the
  fixture's global container_names), image-by-ID, exit-code-from (42
  and 143), port ranges, stdout/stderr separation (StderrContains joins
  the vocabulary) and logging-driver reconfiguration (now locking the
  Recreated expectation). The Ctrl-C dependency test stays legacy.
- Fixtures start-fail/, resources/, stop/, port-range/, stdout-stderr/,
  logging-driver/, profiles/ and most of dependencies/ and run-test/
  are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 9409c461f7 test(e2e): publish-check scenarios — WithStdin answers the prompts
The dry-run publish checks split into nine scenarios, one per model
and refusal reason; the prompt text, refusal exit code (130) and
publication report are the observables. WithStdin joins the Action
modifiers to feed prompt answers. TestPublish stays legacy: it drives
a real registry container and loads oci:// projects with multiple -f
files. Only the oci/ fixture subtree it needs survives.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof d38fa3431a test(e2e): cp, environment and config scenarios
- cp: the twelve copy directions become one scenario; Dir() joins the
  Scenario API for actions that exchange files with the host, and
  FileContains observes what landed there. The files copied in and out
  live in the txtar archive instead of writing into the fixture tree.
- environment: TestEnvPriority splits into three scenarios, one per
  compose model (environment section, bare, env_file section), each
  declaring its .env variants inline; StdoutContains joins the
  vocabulary so container output cannot collide with progress noise
  (the unused framework predicate of the same name is removed). The
  fixed 'env-compose-priority' image name becomes project-scoped.
- config: rendering assertions move to scenario steps; the config
  output is the observable here by definition.
  TestConfigHashMatchesContainerLabel stays legacy: it cross-references
  hashes captured at runtime, which checks cannot hold.
- Fixtures cp-test/, environment/ and config/ are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 816709d877 test(e2e): exec and lifecycle-hook scenarios
- exec: exit-code propagation, -e env forwarding and one-off targeting
  become two scenarios with an inline model.
- hooks: the thirteen post_start/pre_stop/pre_start tests migrate. The
  volume probe is a named action (probeVolume); wc -l counts are locked
  by matching the exact '1 /mnt/tokens.txt' line instead of parsing.
  The spec-change scenario replaces its two fixture files with one
  model interpolating ${HOOK_VERSION}, so the changed spec — and the
  Recreated expectation it implies — is visible in the test source.
  pre_stop scenarios drop the fixture's fixed 'sample-data' volume
  name, which parallel projects used to share.
  Fixtures hooks/ and pre_start/ are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 54caa0775c test(e2e): pull and network scenarios
- pull: the CLI's Pulled/Skipped report is the observable, so the five
  scenarios (always-pull, present-locally, build-only, failure,
  --ignore-pull-failures) legitimately match output; models are inline
  and the whole compose-pull fixture tree — including the orphaned
  duplicate-images directory no test referenced — is removed.
- networks: aliases, links, ipam, network_mode: service, subnet change,
  mac_address and interface_name become scenarios. The subnet-change
  scenario locks what actually happens: the network is replaced but the
  container is reconnected, not recreated (NotRecreated), which the
  legacy test never distinguished. TestNetworks (fixed host ports,
  HTTP fixture services) and TestNetworkRecreate (OR-shaped output
  assertion) stay legacy.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof cd1c0ca6ca test(e2e): configs, commit, export and logs scenarios
ImageExists and FileExists join the vocabulary, giving commit and
export a real observable: the legacy tests only asserted the commands
exited 0, the scenarios verify the committed image and the exported
archive actually exist. Both also drop their fixed image tags and
cwd-relative tar outputs for project-scoped tags and temp files, so
parallel runs cannot collide and nothing is left behind in the repo.

TestLocalComposeLogs waits for the echo service to exit before reading
logs (Eventually), closing the race the legacy test tolerated. The
follow and large-logs tests stay legacy: both drive a long-running
command in the background. ps_test.go stays legacy as well — it
verifies ps table/JSON formatting, which is inherently output-shaped.

Fixtures configs/, commit/ and export/ are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 3923931b8c test(e2e): scale, secrets and env_file scenarios
ServiceScale and ReplicaNumbers join the vocabulary: replica counts and
numbering read from container labels replace counting 'Started' lines
in the CLI output.

- TestScaleDownNoRecreate's final assertion now actually runs: the
  legacy test built the scale-down command without executing it, so
  'scale down removes obsolete replicas' was never exercised; the
  scenario locks the real behavior (stale replicas dropped, up-to-date
  ones kept, whatever their numbers).
- secrets scenarios inline the include-based project (compose.yaml,
  child project and env_file as one txtar archive).
- TestRawEnvFile and TestRunEnvFile inline their env files;
  TestUnusedMissingEnvFile stays legacy for its completion assertions,
  which drive the plugin/standalone binaries directly.
- Fixtures scale/, env-secret/ and dotenv/raw are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof fe17e31be0 test(e2e): lifecycle scenarios — cascade, profiles, healthcheck, recreate
Vocabulary grows with Recreated (the mirror of NotRecreated),
ServiceHealthy (healthcheck status observed from inspect) and a Within
action modifier bounding blocking commands like up --wait, replacing
the hand-rolled goroutine+timer of the legacy TestStartInterval.

- cascade: abort-on-container-exit/failure scenarios inline the model;
  exit-code propagation (111) is locked with ExitCode. The one-off
  variant stays legacy — it drives a long-running up in the background.
- profiles: the four scenarios observe which services materialize
  instead of grepping ps output; the dotenv variant declares its .env
  inline, making the activation mechanism visible.
- TestRecreateWithNoDeps now asserts both sides: the service is
  replaced (Recreated) and the healthy dependency is left alone.
- Fixtures start_interval/, profiles/compose.yaml+test-profile.env and
  recreate-no-deps.yaml are removed; cascade/ stays for the legacy
  one-off test.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 6bc1b36516 test(e2e): expose, container_name, ipc and wait scenarios
- TestExposeRange keeps its regression link (#13378), the invariant
  being simply that up accepts a port range in expose.
- TestUpContainerNameConflict interpolates a project-scoped
  container_name instead of the fixture's global 'test', so parallel
  runs cannot collide on it.
- TestIPC demonstrates the CLI() escape hatch: the external container
  the 'container:' mode points at is created eagerly before the steps,
  so its id is available to the declarative checks. (Runs only in CI:
  ipc container: mode is rejected by Docker Desktop and dind daemons —
  'restricted host mount' — for the legacy test as well.)
- wait scenarios observe which services exited instead of trusting the
  command's silence; TestWaitOnInfinity stays legacy, the DSL has no
  notion of a still-running command. The wait fixture stays for it;
  ipc-test and container_name fixtures are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof a33cc37ad9 test(e2e): TestRemoveOrphans states its real intent — COMPOSE_REMOVE_ORPHANS from .env
The legacy test read as 'up removes leftover one-offs', but plain up
only warns about them: the removal it asserted came from the fixture's
.env declaring COMPOSE_REMOVE_ORPHANS=true (added by 'restore
setEnvWithDotEnv', d3d378b92) — an invariant the fixture kept invisible.
The scenario declares both files inline as a txtar archive, so the
mechanism under test is in plain sight, and observes the one-off's
removal from container state rather than ps output.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof a725a8ae04 test(e2e): one-off vocabulary — service checks skip run containers
Snapshots now record whether a container is a one-off (run) container,
plus its start time. Service-facing checks (ServiceState, NotRecreated,
label checks) observe only long-lived containers; three checks join the
vocabulary for the one-off side: OneOffState, OneOffsUntouched (same
containers, same state, same start time — catches an unwanted restart,
which message matching never could) and OneOffsRemoved (errors if there
was nothing to remove, rather than pass vacuously).

TestStartStopWithOneOffs becomes the demonstrator: stop/start/restart
act on services and leave the one-off untouched, down --remove-orphans
takes it away.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 695a2a759a test(e2e): start/stop scenarios observe container state, not CLI chatter
Migrate the state-friendly start/stop tests to the Scenario DSL:

- TestStartStop: stop halts containers in place, start brings the same
  ones back (NotRecreated); the ls checks match the project name exactly
  in the JSON output, immune to the prefix collisions the other
  start-stop project names would cause under parallel runs.
- TestStartStopWithDependencies: dependency start/stop propagation read
  from container state instead of 'Container X Started' messages.
- TestUpNoDeps replaces the 'Up no-deps links' subtest as its own
  scenario: up --no-deps must not create the linked dependency.
- TestStartSingleServiceAndDependency: create/start walk the dependency
  chain and nothing else — now asserting the unrelated services have no
  container at all, stronger than the legacy message matching.

The one-off and multi-file tests stay legacy: the vocabulary has no
one-off awareness yet and a scenario holds a single compose file.
The links and start-stop-deps fixtures become unused and are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 5baeadf929 test(e2e): no-deps scenarios — the donor container's absence is the observable
Migrate the volumes_from and network_mode:service donor tests to the
Scenario DSL, inlining the compose model and dropping the no-deps
fixtures. Each scenario removes the donor container behind compose's
back and expects up --no-deps to be rejected with the 'container
missing' error — the CLI's reported decision being the observable.

ServiceNotCreated joins the check vocabulary: it asserts a service has
no container at all, here proving the donor really is gone before the
failing step.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 12288a7fff test(e2e): pause scenarios — paused is a container state, ExitCode locks error paths
Migrate the four pause tests to the Scenario DSL: the invariant is the
container state (paused/running), not an HTTP timeout, so the scenarios
observe state and drop the port-mapping helpers and the pause fixture.

ExitCode joins the check vocabulary for error-path steps, paired with
MayFail. TestPauseServiceDoesNotExist now locks the current behavior —
pause of an unknown service is rejected with 'no such service' — which
resolves the TODO the legacy test carried: it asserted exit 0 while
wishing for an error, and the CLI has since started validating selected
services.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof b2e3deb18e test(e2e): testdata/<TestName>/ anchors each scenario's project files
Each scenario resolves its project files from testdata/<TestName>/ by
convention: standalone compose files in their native format, directly
runnable with docker compose -f, copied to a temporary directory so the
committed files are never mutated. Subtests map to nested directories.

Ownership is strictly one test per directory: a check fails the suite on
any testdata directory no test owns, which is what keeps this from
degrading into a catch-all fixtures directory — no sharing, no
leftovers after a rename.

The inline Compose() declaration is removed; the demonstrator scenarios
move their models to testdata, including the multi-file build context
of TestUpBuildUnchangedContext (compose.yaml + Dockerfile + marker).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De LoofandClaude Fable 5 fa9b31c2fa test(e2e): checks.go as the reviewed vocabulary, SCENARIO.md codifies the contract
Checks are the shared vocabulary between scenarios: move them to a
dedicated checks.go so the whole vocabulary is read — and reviewed — as
one file, independently of the tests using it.

SCENARIO.md codifies the rules that so far only lived in the PR
description: one intent = one invariant, step names as behavior
sentences, inline compose model, state-based checks first with
OutputContains as an explicit last resort, new checks generic and named
after their observable — plus how to exploit the failure artifacts and
E2E_KEEP_FAILED. AGENTS.md points new e2e work at it.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De LoofandClaude Fable 5 9ff4693312 test(e2e): dump failure artifacts, E2E_KEEP_FAILED keeps the project alive
On failure, a scenario now writes the untruncated diagnostic material to a
stable per-project directory (compose.yaml, each step's full command and
output, project containers, engine events, full container logs, per-step
state snapshots), announced at the top of the failure report. The inline
report stays truncated for readability; the artifacts let a failure be
diagnosed without re-running the scenario.

E2E_KEEP_FAILED=1 skips the teardown of a failed scenario so the live
project state remains inspectable.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00