addPreStartHookPulls skipped any hook image already present locally,
regardless of pull policy. As a result a service with pull_policy: always
had its own image force-pulled on every up while its pre_start hook images
were left stale — diverging from both the service image and the `pull`
command path (which already re-pulls hooks under always).
Skip the "already present" shortcut when the parent service is
pull_policy: always, so hook images get the same force-pull treatment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Address review feedback on pre_start hook image resolution:
1. GetDependentImages now skips a hook image equal to the service image
(resolved via GetImageNameOrDefault), so `config --images` no longer
prints a duplicate line and pullRequiredImages no longer schedules a
redundant pull for it.
2. pullRequiredImages (up/create path) now dedups dependent images by
reference via a `scheduled` set, so several hooks/services sharing the
same missing image don't schedule concurrent redundant pulls. The hook
pass moved to a helper (addPreStartHookPulls) to keep complexity in check.
3. The `pull` command no longer skips hook images under `pull_policy: build`.
A hook image is a registry image that can't be built, so only `never`
justifies skipping it — making `pull` consistent with the `up` path.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
pre_start hooks run as ephemeral init containers with their own image
(ServiceHook.Image), but that image was ignored by image resolution:
`config --images` didn't list it, `pull` didn't fetch it, and `up` failed
at runtime with "No such image" when it wasn't already present locally.
Add a GetDependentImages helper that returns a service's pre_start hook
images, and use it wherever service images are collected/pulled:
getLocalImagesDigests, pullRequiredImages (up path), the pull command, and
`config --images`. Hook images inherit the parent service pull policy.
post_start/pre_stop hooks run via ExecCreate inside the service container
and never use hook.Image, so they are intentionally out of scope.
Digest resolution/locking (--resolve-image-digests / --lock-image-digests)
is not covered: compose-go's WithImagesResolved only resolves service.Image
(needs an upstream change), and the --lock-image-digests override merges
pre_start by concatenation, which would duplicate hooks.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
AI_AGENT_DISCLOSURE.md was merged into the repository by accident as
part of a reconcile change (44ac2c94e). With the file permanently
present, the AGENTS.md instruction to "add" it became a no-op and
AI-prepared pull requests no longer carried any disclosure signal.
Remove the committed copy and change the instruction to add the file
with the current date, or refresh the date when the file is already
present, so every AI-prepared change includes the disclosure in its
diff.
Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
collectObservedState indexed networks/volumes by compose label into a
single-valued map, so two live resources sharing a label (e.g. a leftover
after a rename) collided and the "winner" depended on the daemon's list
order — a nondeterministic `up` (spurious create/recreate events, possible
churn) on subsequent runs.
Make collection lossless and move the selection into the reconciler:
- ObservedState.Networks/Volumes become map[string][]Observed*: collection
records every label-sharing resource and makes no premature choice.
- selectNetwork/selectVolume deterministically pick the resource matching
the desired name (else the lexicographically smallest), returning the
others as orphans.
- reconcile resolves the observed state once (resolveObserved) into
single-valued resolvedNetworks/resolvedVolumes used everywhere, and warns
about orphans instead of acting on them — they are left untouched because
removing them could drop data or break unrelated workloads.
Adds selection unit tests, a collector aggregation test and a reconcile
conflict test (deterministic no-op + orphan warning across list orders).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
A network rename does not require removing the old network — the new one
has a different name and is created independently. Yet the old removal
could block the whole operation: NetworkRemove fails when non-Compose
containers are still attached, and CreateNetwork depended on it.
Split the rename path from the same-name divergence path:
- Rename: CreateNetwork no longer depends on RemoveNetwork; the container
migration proceeds regardless. RemoveNetwork is marked best-effort and,
if the network is still in use (reported as a conflict), is skipped with
a warning instead of failing. Any other error (transport, Moby API) is
still propagated.
- Same-name divergence keeps the mandatory remove-before-create ordering.
Adds an Operation.BestEffort flag, honored by execRemoveNetwork, plus
reconcile and executor tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Address review findings on the network reconcile migration:
- createNetwork now treats a NetworkCreate conflict as success. A
concurrent `docker compose up|run` can create the same network in the
TOCTOU window between the observed-state snapshot and the create call;
the previous ensureNetwork retried on conflict, the plain create must
not fail hard.
- discoverUnmanagedNetworks/Volumes preserve the config-hash when the live
resource is owned by this project (project label present, key label
absent — e.g. written by an older Compose) so genuine divergence is
still detected. For resources we don't own the hash stays empty and they
are reused untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Treat a network rename (observed.Name != desired.Name) as a recreation
rather than an additive create: the old network is removed, the new one
created, and attached containers are migrated onto it (reconnected), so
they no longer stay on the previous network until recreated for another
reason.
Networks carry no data, so removing the previous network — instead of
leaving it dangling alongside the new one under the same compose label —
is safe and keeps subsequent runs deterministic. This is a marginal
behavior change from previous Compose releases (which created the new
network and left the old attachments in place) in exchange for the more
logical outcome.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Mirror the volume reconciliation work (#13962) for networks: move network
divergence detection and recreation out of the imperative pre-reconcile
path (ensureNetwork/resolveOrCreateNetwork/removeDivergedNetwork) into the
reconciliation plan.
- reconcileNetworks now owns creation of missing networks and, for a
network whose config-hash diverged, an explicit recreation sequence
(no user confirmation: recreating a network is not destructive):
stop containers -> disconnect -> remove network -> create network ->
reconnect containers. Attached containers keep their identity (they are
reconnected, not recreated), matching the previous behavior. If a
container is independently recreated by reconcileContainers, its removal
is ordered after the reconnect so they don't race.
- Renaming a network creates the new one additively and leaves the old one
untouched.
- collectObservedState discovers legacy/unlabeled networks by name and
records them as unmanaged matches (empty config-hash) so the reconciler
reuses them untouched; ownership warnings move to warnUnmanagedNetworks.
checkExternalNetworks keeps external-network validation/resolution.
- execCreateNetwork now issues a plain createNetwork; the imperative
ensureNetwork/resolveOrCreateNetwork/removeDivergedNetwork and the
connect/disconnect helpers are removed.
Adds reconcile, observed-state and executor tests covering network
create/diverge/rename, the entangled diverge+recreate case, legacy
by-name discovery and the ownership warnings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Rely on compose-go WithImagesResolved, which now resolves dependent
images — `type: image` volume sources and pre_start hook images —
with its already-digested guard, per-call memoization and
sibling-service detection (compose-spec/compose-go#894, #899), rather
than duplicating resolution logic CLI-side. The interpolated path gets
this for free; --no-interpolate maps the raw model onto a
pseudo-project keyed by service names to reuse the same resolution,
and --lock-image-digests keeps type:image volumes in its output.
pre_start hooks can't be carried into the lock override (hook lists
are appended on merge), so generating a lock warns that hook images
stay unpinned there.
As a side effect, `compose publish` now fails fast on unresolvable
dependent images.
Fixes#13827
Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
The additive rename path created the new volume but kept the old name in
the observed state, so hasVolumeMismatch never fired: existing containers
stayed on the old volume while fresh replicas mounted the new one
(split-brain), and later runs picked a nondeterministic winner between the
two equally labelled volumes.
Rewrite the observed volume name to the desired one after planning the
"renamed" create, so reconcileContainers migrates the existing containers
onto the new volume in the same up — restoring parity with the old
ensureVolume path — while still leaving the old volume and its data intact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Two edge regressions from the switch to a label-scoped observed state,
both reported against the old ensureVolume path:
- A same-named volume created manually or by another project (no compose
label) was invisible to the observed state, so a VolumeCreate was
planned on every up: a hard failure if the driver differed, spurious
Creating/Created events otherwise. collectObservedState now discovers
such volumes by name (pre-label Compose semantics) and records them as
unmanaged matches with an empty config-hash, so the reconciler reuses
them untouched. The ownership warnings move to warnUnmanagedVolumes,
driven off the observed state; checkVolumes shrinks to external-only
validation (checkExternalVolumes).
- Renaming a volume hit the diverged path and, with up -y, deleted the
old volume and its data (VolumeHash includes Name), where it previously
just created the new one. When observed.Name != desired.Name the volume
is now created additively, leaving the old one untouched, with no prompt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
servicesUsingVolume only matched services mounting the volume directly, so
a service reaching it through volumes_from was not stopped/removed before
RemoveVolume. Docker materializes the inherited mount on the consumer's
container, so its removal would fail with "volume in use". Compute the
transitive volumes_from closure so every container referencing the volume
is removed first. (network_mode/ipc/pid: service:x share namespaces, not
mounts, and are intentionally excluded.)
Also reassign the result of Labels.Add in createVolume: it mutates in
place only when the map is non-nil, so discarding the return would drop
the config-hash label for a volume with no CustomLabels.
Addresses review feedback: documents why observed.Containers is cleared
without touching the observedContainersByService hashing snapshot, and
strengthens the cascade tests to assert the full plan ordering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Move volume divergence detection and recreation out of the imperative
pre-reconcile path (ensureVolume/removeDivergedVolume) and into the
reconciliation plan, activating the dormant planRecreateVolume seam.
A diverged volume now produces an explicit, forward-only sequence:
stop containers -> remove containers -> remove volume -> create volume
-> create containers. Container re-creation is delegated to
reconcileContainers (affected services are cleared from the observed
snapshot so they are scheduled fresh, gated on the CreateVolume node),
and the recreation cascades to namespace/volume-sharing dependents.
User confirmation (recreate, data will be lost) is consulted while
building the plan via reconciler.prompt; declining leaves the volume
untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
The alpine 3.22 variant was removed.
Also updating the macOS SDK version to avoid errors when building
on alpine 3.23;
1.107 GO111MODULE=on go build -trimpath -tags "fsnotify,e2e" -ldflags "-w -X github.com/docker/compose/v5/internal.Version=61203c8" -o "/out/docker-compose" ./cmd
37.96 # runtime/cgo
37.96 clang: error: overriding deployment version from '10.16' to '11.0' [-Werror,-Woverriding-deployment-version]
39.77 make: *** [Makefile:61: build] Error 1
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Move the project guide to AGENTS.md (the agents.md convention used
by Claude Code, Codex, Cursor, etc.) so all coding agents pick it
up — not just Claude. Keep CLAUDE.md as a symlink so Claude-specific
tooling keeps working unchanged.
Add an "Issue and PR Guidelines" section asking AI agents to disclose
their involvement when filing PRs or issues on a human's behalf,
mirroring the pattern used in ghostty-org/ghostty's AGENTS.md.
Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
`docker compose -f oci://<insecure-registry>/... up` failed against a
plain-HTTP registry unless --yes was passed:
failed to pull OCI resource "localhost:5000/test:interpolated":
Head "https://localhost:5000/v2/test/manifests/interpolated":
http: server gave HTTP response to HTTPS client
`up` loads the project twice. The first load goes through ToProject,
which built its OCI options from --insecure-registry correctly. Without
--yes, checksForRemoteStack then calls promptForInterpolatedVariables,
which re-loads the project through ToModel to list the interpolation
variables. That second load builds its own resource loaders via
remoteLoaders, and those passed an empty api.OCIOptions{}, dropping the
flag. Since the OCI loader always performs a network resolve, the
re-load spoke HTTPS to a plain-HTTP registry and failed before the
prompt could be shown.
The two construction sites had drifted apart, so rather than patching
the second one, both now share ProjectOptions.ociOptions(). `config`
and `viz` use the same ToModel path and are fixed as well.
Covered by an e2e case in TestPublish, which already runs an insecure
registry and an oci:// round-trip: it publishes a fixture carrying an
interpolation variable so the prompt fires, then runs `up` without
--yes and declines, asserting the re-load does not fail with
"server gave HTTP response to HTTPS client".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Domantas Petrauskas <dom.petrauskas@gmail.com>
With the containerd image store and BuildKit provenance attestations
(the default), a built image is stored as an attested index whose
top-level digest also covers the attestation manifest. That digest
churns on every build even when the runnable content is unchanged,
so compose recreated containers on every `up --build`.
Compare the digest of the "image" kind manifest instead, selected for
the target platform and restricted to locally available manifests, so
it is deterministic and reflects only config + layers. Both the build
and up sides of the staleness check go through the same selection, and
registry-only images keep the Bake-reported digest.
Fixes#13636
Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
`docker compose config --no-interpolate <service>` and
`docker compose config --variables <service>` load the raw model
without applying service filtering, so the full model is rendered
regardless of the services passed as arguments. Filtering will not
be supported on these paths, so emit a warning to make sure users
are no longer misled by silently ignored arguments.
Fixes#13614
Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
The project was sunset;
> After more than a decade of serving the ecosystem, the time
> has come to sunset Go Report Card. Following the loss of our
> primary infrastructure sponsor, maintaining the web app is
> no longer sustainable.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
- Replace ncipollo/release-action with softprops/action-gh-release
(v3.0.1, SHA-pinned), the action used across Docker repositories;
input mapping is one-to-one.
- Duplicate the artifact listing/type checks into binary-finalize:
one copy inspects the artifact as assembled, on every push/PR (lost
when the tag guard moved to job level); the release-job copy still
inspects what was downloaded and gets attached to the release.
- Set overwrite_files: false so a job re-run never replaces assets
already uploaded to an existing release: unlike ncipollo, softprops
updates an existing release instead of failing. A re-run may still
refresh the release name/notes.
Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
The pinned codeql-action/upload-sarif v2 (v2.28.1) falls in the
vulnerable range of CVE-2025-24362 and the v2 line has no patched
release, so bump to v3.36.3. Scope the release job to tag refs so its
contents:write token is only minted when a release is actually
created. In merge.yml, drop a dead conditional (workflow only triggers
on push) and pass DOCKERDESKTOP_REPO to github-script via env rather
than inline interpolation, as recommended against script injection.
Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>