Skip to content

refactor(compose): move publish and transform to internal/publish - #14409

Open
ndeloof wants to merge 1 commit into
docker:mainfrom
ndeloof:internal-publish
Open

ndeloof wants to merge 1 commit into
docker:mainfrom
ndeloof:internal-publish

Conversation

@ndeloof

@ndeloof ndeloof commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

What this PR does, in one sentence

Moves publish and its YAML-rewriting helper transform out of the flat pkg/compose package into internal/publish (and internal/publish/transform), behind a small Backend interface.

Context

publish is a leaf of the engine: it pre-checks what an application would leak, builds the OCI layers and pushes them, and nothing else in pkg/compose depends on it. It still lives in the flat package, where it can reach anything on composeService, and pkg/compose/transform is an exported package that only publish uses.

What the PR brings

  • internal/publish declares what it needs from the engine in seven methods (API client, registry credentials, events, dry-run flag, prompt, image digest resolution and push). The image digest resolution and the push stay in pkg/compose and go through the Backend, since pull and push will be extracted separately.
  • composeService implements that interface and keeps a one-line Publish forwarder, still wrapped in the publish progress bracket, so callers do not change.
  • pkg/compose/transform moves to internal/publish/transform; this is the "move transform under internal/" item of the public-surface list of the epic. The only exported symbols leaving the /v5 API are transform.ReplaceEnvFile and transform.ReplaceExtendsFile; no code in the repository other than publish imported them.
  • Behavior is unchanged: the code is moved as is, and the tests move with it with the same assertions (the engine is replaced by a small fake Backend; the real digest resolver stays covered by its own test in pkg/compose). The publish e2e tests pass in standalone and plugin modes.

Why this is the right next brick

It is a self-contained extraction (about 1000 lines, 7-method Backend) that does not depend on the other pending moves (images, build, logs, plan): the pull/push parts it uses are reached through the Backend and can move later without touching it.

Part of #14396

🤖 Generated with Claude Code

@ndeloof
ndeloof requested review from a team as code owners October 28, 2026 07:09

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟢 APPROVE

@codecov

codecov Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.61663% with 135 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/publish/publish.go 72.17% 88 Missing and 45 partials ⚠️
pkg/compose/publish.go 86.66% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟡 NEEDS ATTENTION

One medium-severity bug was found in the new internal/publish package.

if xf == "" {
continue
}
if _, err = os.Stat(service.Extends.File); os.IsNotExist(err) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] os.Stat error on extends file silently ignored when error is not os.IsNotExist

In processFile, when os.Stat(service.Extends.File) returns a non-IsNotExist error (e.g. permission denied, a stale NFS mount, etc.), the error is silently swallowed. Because os.IsNotExist(err) is false, the continue is not taken, and execution falls through to:

hash := fmt.Sprintf("%x.yaml", sha256.Sum256([]byte(xf)))
extFiles[xf] = hash
f, err = transform.ReplaceExtendsFile(f, name, hash)

The inaccessible file is treated as a valid local extends resource: its path is registered for upload and rewritten in the YAML — even though it cannot be read. This means the published OCI artifact will reference a layer that either fails to build or silently omits the extends content.

The fix is to add an explicit guard after the IsNotExist branch:

if _, err = os.Stat(service.Extends.File); os.IsNotExist(err) {
    // No local file, while we loaded the project successfully: This is actually a remote resource
    continue
} else if err != nil {
    return nil, fmt.Errorf("checking extends file %s: %w", service.Extends.File, err)
}
Confidence Score
🟢 strong 100/100

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants