Skip to content

fix compose feature image tagging#655

Merged
skevetter merged 9 commits intoskevetter:mainfrom
YassineElbouchaibi:codex/compose-feature-derived-image-654
Apr 6, 2026
Merged

fix compose feature image tagging#655
skevetter merged 9 commits intoskevetter:mainfrom
YassineElbouchaibi:codex/compose-feature-derived-image-654

Conversation

@YassineElbouchaibi
Copy link
Copy Markdown

@YassineElbouchaibi YassineElbouchaibi commented Mar 29, 2026

Summary

This isolates Compose feature builds from shared image tags when a service is image-backed.

Fixes #654

Public repro: https://github.com/YassineElbouchaibi/devpod-compose-image-stain-issue-654

What changed

  • compute a workspace-local derived image name for image-backed Compose services when Features are applied
  • keep the generated feature Dockerfile FROM pointed at the original Compose image:
  • tag the feature-extended build output to the derived image instead of mutating the shared base image tag
  • add unit coverage for the image name selection and compose service generation
  • add an e2e regression that proves two projects can share the same base image tag without leaking Features into each other

Why

The Compose feature extension path was reusing the original service image: as the output image name. For image-backed services, that retagged the shared base image locally after Features were layered on top of it. Any other project using the same base image tag could then inherit the wrong tools and packages.

This mirrors the image-backed Compose behavior used by devcontainers/cli, which avoids retagging a previously pulled image and instead runs the workspace from a derived local image.

Validation

  • go test ./pkg/devcontainer/...
  • go run github.com/onsi/ginkgo/v2/ginkgo@latest --focus='does not retag shared image when applying features to image backed services' ./e2e

Made with Codex + GPT-5.4.

Summary by CodeRabbit

  • Tests

    • Added an end-to-end test validating shared-image immutability and feature isolation across two Compose projects.
    • Added unit tests covering compose image-name selection and service build/extension behavior.
  • Chores

    • Standardized compose image naming and build/extend flow with improved error handling.
    • Added Compose/devcontainer fixtures and test helpers to support shared-image scenarios.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 29, 2026

Warning

Rate limit exceeded

@skevetter has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 18 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 18 minutes and 18 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8cd2b678-0244-4435-b229-5c1bdf890329

📥 Commits

Reviewing files that changed from the base of the PR and between 5e5a300 and 7411f3b.

📒 Files selected for processing (5)
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/devcontainer.json
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/devcontainer.json
  • pkg/devcontainer/build.go
  • pkg/devcontainer/compose.go
  • pkg/devcontainer/compose_test.go
📝 Walkthrough

Walkthrough

Compute a workspace-specific build image name for Compose services when features are applied, thread that name through compose build/service creation to avoid mutating the original compose image: tag, add e2e fixtures/tests validating isolation, and add Docker image inspect/reset helpers.

Changes

Cohort / File(s) Summary
E2E Helper Utilities
e2e/tests/up-docker-compose/helper.go
Added helpers to inspect a Docker image ID and to reset a tagged image (pull source, best-effort remove existing tag, retag). Ensured testdataPath is absolute in setupWorkspace.
Shared Image Test Fixtures (Project A)
e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/compose.yaml, .../compose.override.yaml, .../devcontainer.json
Added compose/devcontainer configuration for project A using devpod-e2e-compose-shared-base:latest, sets REPRO_PROJECT=project-a, and enables the github-cli feature.
Shared Image Test Fixtures (Project B)
e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/compose.yaml, .../compose.override.yaml, .../devcontainer.json
Added compose/devcontainer configuration for project B using the same shared base image, sets REPRO_PROJECT=project-b, and enables the node feature.
Shared Image E2E Test
e2e/tests/up-docker-compose/up_docker_compose.go
New Ginkgo spec that resets a shared image tag, runs project A then B, asserts the shared tag's image ID remains unchanged, and verifies features do not cross-contaminate between projects.
Compose Build Image Naming & Flow
pkg/devcontainer/compose.go
Refactored multiple functions to return structs, added composeBuildImageName(...) and composeBuildInfo/composeExtendResult structs, threaded buildImageName through build/extend/create flows, and added explicit error handling when neither Image nor Build is present.
Compose Logic Tests
pkg/devcontainer/compose_test.go
Converted tests to a testify suite, added table-driven tests for composeBuildImageName(...), and added tests ensuring createComposeService(...) uses provided build image name and sets build fields/build-args correctly.
Build Flow Integration
pkg/devcontainer/build.go
Adjusted buildDevImageCompose to consume the new single extend result struct and set config.BuildInfo fields from it.

Sequence Diagram(s)

sequenceDiagram
  participant DevPod as DevPod CLI
  participant NameGen as composeBuildImageName
  participant Builder as Docker Engine
  participant ComposeSvc as Generated Compose Service

  DevPod->>NameGen: Request build image name (composeService, hasFeatures)
  NameGen-->>DevPod: return workspace-specific buildImageName
  DevPod->>Builder: Build feature image FROM original-image tag=buildImageName
  Builder-->>DevPod: build result (image ID)
  DevPod->>ComposeSvc: set service.Image = buildImageName (leave original image tag unchanged)
  ComposeSvc-->>DevPod: compose up uses derived image
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and directly summarizes the main fix: isolating image tagging for Compose services with Features to prevent shared base image mutation.
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from issue #654: derives workspace-specific image names for image-backed services with Features, prevents shared image mutation, keeps original image reference untouched, and adds comprehensive e2e/unit test coverage validating feature isolation.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #654: core Compose feature logic, test infrastructure (suite pattern refactor), test fixtures for e2e validation, and helper methods for image inspection/tagging required for the fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@YassineElbouchaibi YassineElbouchaibi changed the title [codex] fix compose feature image tagging fix compose feature image tagging Mar 29, 2026
@YassineElbouchaibi YassineElbouchaibi marked this pull request as ready for review March 29, 2026 21:37
Copilot AI review requested due to automatic review settings March 29, 2026 21:37
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a Compose + Features bug where image-backed services would retag the original image: (a shared local tag), causing feature “leakage” across projects. The fix introduces a workspace-local derived image name for feature-extended builds while keeping the generated feature Dockerfile FROM pinned to the original Compose image.

Changes:

  • Derive a workspace-scoped output image name for image-backed Compose services when Features are applied, instead of mutating the shared image: tag.
  • Thread the computed build image name through the generated feature-build Compose service.
  • Add unit and e2e regression coverage to ensure shared tags are not retagged across projects.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
pkg/devcontainer/compose.go Computes a derived image name for image-backed + Features and uses it as the build output tag in the generated Compose override.
pkg/devcontainer/compose_test.go Adds unit tests for image name selection and ensuring generated compose build service uses the provided build image name.
e2e/tests/up-docker-compose/up_docker_compose.go Adds an end-to-end regression ensuring two projects sharing the same base tag don’t leak Features into each other.
e2e/tests/up-docker-compose/helper.go Adds helpers to inspect/reset image tags and makes workspace testdata paths absolute-relative to the initial working directory.
e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/devcontainer.json New e2e fixture: project A applies github-cli feature on a shared base image tag.
e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/compose.yaml New e2e fixture Compose file referencing the shared base image tag.
e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/compose.override.yaml New e2e fixture override env for project A.
e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/devcontainer.json New e2e fixture: project B applies node feature on the same shared base image tag.
e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/compose.yaml New e2e fixture Compose file referencing the shared base image tag.
e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/compose.override.yaml New e2e fixture override env for project B.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
e2e/tests/up-docker-compose/up_docker_compose.go (1)

230-233: Make the negative checks prove tool absence, not just “some error”.

These assertions pass on any docker run/SSH failure, so the regression can go green even if the workspace is simply unhealthy. Prefer checking command -v gh / command -v node in a known-good shell, or assert that stderr matches the expected “not found” failure.

Also applies to: 247-251, 258-262

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/up-docker-compose/up_docker_compose.go` around lines 230 - 233,
Replace the broad error assertion that any docker/SSH failure counts as tool
absence by running a known-good shell inside the container and checking for the
tool explicitly: instead of exec.CommandContext(ctx, "docker", "run", "--rm",
sharedImage, "gh", "--version") and asserting err != nil on hostGhCmd/hostGhOut,
run docker run --rm sharedImage sh -lc "command -v gh" (and similarly for node
in the other blocks referenced) and assert the command exits non‑zero or returns
an empty stdout (or assert stderr contains the expected “not found” text) so the
test proves gh/node are truly missing rather than any other docker failure;
update the assertions at the other locations (lines ~247-251 and ~258-262) to
follow the same pattern and reference their command variables accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/tests/up-docker-compose/up_docker_compose.go`:
- Around line 188-195: The test uses a moving tag for the baseline image
(constant sourceImage in up_docker_compose.go), causing non-deterministic
failures; update the sourceImage constant to a specific immutable image digest
(replace "mcr.microsoft.com/devcontainers/base:ubuntu-24.04" with the
corresponding "@sha256:..." digest) so the test always uses the exact image
expected by the negative assertions, leaving other constants like sharedImage
and projectAPath/projectBPath unchanged.

---

Nitpick comments:
In `@e2e/tests/up-docker-compose/up_docker_compose.go`:
- Around line 230-233: Replace the broad error assertion that any docker/SSH
failure counts as tool absence by running a known-good shell inside the
container and checking for the tool explicitly: instead of
exec.CommandContext(ctx, "docker", "run", "--rm", sharedImage, "gh",
"--version") and asserting err != nil on hostGhCmd/hostGhOut, run docker run
--rm sharedImage sh -lc "command -v gh" (and similarly for node in the other
blocks referenced) and assert the command exits non‑zero or returns an empty
stdout (or assert stderr contains the expected “not found” text) so the test
proves gh/node are truly missing rather than any other docker failure; update
the assertions at the other locations (lines ~247-251 and ~258-262) to follow
the same pattern and reference their command variables accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 92cdb157-47eb-4cec-b0ed-d78355662e58

📥 Commits

Reviewing files that changed from the base of the PR and between 31f783b and 66e6b6d.

📒 Files selected for processing (10)
  • e2e/tests/up-docker-compose/helper.go
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/compose.override.yaml
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/compose.yaml
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/devcontainer.json
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/compose.override.yaml
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/compose.yaml
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/devcontainer.json
  • e2e/tests/up-docker-compose/up_docker_compose.go
  • pkg/devcontainer/compose.go
  • pkg/devcontainer/compose_test.go

@YassineElbouchaibi YassineElbouchaibi force-pushed the codex/compose-feature-derived-image-654 branch from 11ed196 to 05949b0 Compare March 31, 2026 01:00
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
e2e/tests/up-docker-compose/helper.go (1)

59-80: Consider guarding sourceImage == targetImage in reset helper.

Defensive early-return avoids self-untagging when caller passes the same value for source and target.

Suggested hardening
 func (btc *baseTestContext) resetTaggedImage(
 	ctx context.Context,
 	sourceImage, targetImage string,
 ) error {
 	if err := btc.dockerHelper.Pull(ctx, sourceImage, nil, io.Discard, io.Discard); err != nil {
 		return err
 	}
+	if sourceImage == targetImage {
+		return nil
+	}
 	_ = btc.dockerHelper.Run(
 		ctx,
 		[]string{"image", "rm", "-f", targetImage},
 		nil,
 		io.Discard,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/up-docker-compose/helper.go` around lines 59 - 80, The
resetTaggedImage helper should guard against sourceImage == targetImage to avoid
removing the image when re-tagging to the same name: inside resetTaggedImage
(method on baseTestContext) add an early-return (nil) when sourceImage equals
targetImage before calling dockerHelper.Pull or running image rm/tag; this
prevents the image removal Run call (dockerHelper.Run with ["image","rm","-f",
targetImage]) from accidentally untagging/self-deleting the same image.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@e2e/tests/up-docker-compose/helper.go`:
- Around line 59-80: The resetTaggedImage helper should guard against
sourceImage == targetImage to avoid removing the image when re-tagging to the
same name: inside resetTaggedImage (method on baseTestContext) add an
early-return (nil) when sourceImage equals targetImage before calling
dockerHelper.Pull or running image rm/tag; this prevents the image removal Run
call (dockerHelper.Run with ["image","rm","-f", targetImage]) from accidentally
untagging/self-deleting the same image.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5cadcce5-69ba-4e4e-88d5-7b793ff7dfab

📥 Commits

Reviewing files that changed from the base of the PR and between 11ed196 and 05949b0.

📒 Files selected for processing (10)
  • e2e/tests/up-docker-compose/helper.go
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/compose.override.yaml
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/compose.yaml
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/devcontainer.json
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/compose.override.yaml
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/compose.yaml
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/devcontainer.json
  • e2e/tests/up-docker-compose/up_docker_compose.go
  • pkg/devcontainer/compose.go
  • pkg/devcontainer/compose_test.go
✅ Files skipped from review due to trivial changes (8)
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/compose.yaml
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/compose.override.yaml
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/devcontainer.json
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/compose.yaml
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/devcontainer.json
  • pkg/devcontainer/compose_test.go
  • e2e/tests/up-docker-compose/up_docker_compose.go
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/compose.override.yaml

YassineElbouchaibi and others added 7 commits March 31, 2026 21:24
Move TestStripDigestFromImageRef, TestComposeBuildImageName, and
TestCreateComposeServiceUsesBuildImageName into a ComposeSuite using
stretchr/testify suite+assert, matching the PrepareBuildContextSuite
pattern already in this file.
Pin github-cli to 1.1 and node to 1.7 in the shared-image e2e test
fixtures to prevent flaky failures from floating major-version refs
resolving to untested new minor versions.
- Call composeBuildImageName once after determining hasFeatures, instead
  of calling it twice (first with false, then overwriting with true).
- Sanitize composeService.Image by stripping newlines before embedding
  it in the synthetic Dockerfile FROM line.
- Return an explicit error when a compose service has neither image nor
  build configuration, preventing an invalid empty FROM.
- Add test cases documenting image+build+features behavior.
Replace multi-value returns with result structs for four functions that
exceed the revive 3-return-value limit:
- dockerComposeProjectFiles -> composeProjectFiles
- prepareComposeBuildInfo -> composeBuildInfo
- buildAndExtendDockerCompose -> composeExtendResult
- checkForPersistedFile -> persistedFileResult

Also remove the unused error return from checkForPersistedFile (flagged
by unparam, as it never returned a non-nil error).
@github-actions github-actions bot added size/xl and removed size/l labels Apr 4, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/devcontainer/build.go (1)

509-516: ⚠️ Potential issue | 🟠 Major

Potential issue: BuildInfo.ImageName may be empty when buildImageName is empty.

When extendResult.buildImageName is empty, currentImageName is correctly set to originalImageName (lines 491-494), but BuildInfo.ImageName on line 512 would still be empty. Based on the relevant code snippets, buildInfo.ImageName is used in prebuild.go for tagging and in single.go for run options, which could cause issues.

🐛 Proposed fix: use currentImageName for BuildInfo.ImageName
 	return &config.BuildInfo{
 		ImageDetails:  imageDetails,
 		ImageMetadata: extendResult.imageMetadata,
-		ImageName:     extendResult.buildImageName,
+		ImageName:     currentImageName,
 		PrebuildHash:  imageTag,
 		RegistryCache: options.RegistryCache,
 		Tags:          options.Tag,
 	}, nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainer/build.go` around lines 509 - 516, The returned
config.BuildInfo currently uses extendResult.buildImageName for ImageName which
can be empty; change it to use the resolved currentImageName (the value chosen
between extendResult.buildImageName and originalImageName) so ImageName is
always populated—update the BuildInfo creation in the function that returns
&config.BuildInfo{...} to set ImageName: currentImageName instead of
extendResult.buildImageName.
🧹 Nitpick comments (1)
pkg/devcontainer/compose.go (1)

672-677: Sanitization handles newline injection, but consider additional validation.

The newline/carriage return stripping prevents basic Dockerfile injection. However, for defense in depth, consider validating that the image reference conforms to expected Docker image reference format (e.g., no shell metacharacters, spaces, or other unexpected characters).

💡 Optional: Use a regex to validate image reference format
-			sanitizedImage := strings.ReplaceAll(
-				strings.ReplaceAll(composeService.Image, "\n", ""),
-				"\r",
-				"",
-			)
+			// Validate and sanitize image reference
+			sanitizedImage := strings.TrimSpace(strings.ReplaceAll(
+				strings.ReplaceAll(composeService.Image, "\n", ""),
+				"\r",
+				"",
+			))
+			if sanitizedImage == "" {
+				return composeExtendResult{}, fmt.Errorf(
+					"compose service %q has empty image after sanitization",
+					composeService.Name,
+				)
+			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainer/compose.go` around lines 672 - 677, The code currently
strips newlines from composeService.Image and writes it into dockerfileContents
using buildTarget, but it lacks stricter validation; add an image-reference
validation step after computing sanitizedImage (before using it in
dockerfileContents) that rejects or errors on values containing spaces, shell
metacharacters (e.g., ; && | ` $ <>), control characters, or that do not match a
Docker image reference pattern (e.g., optional
domain[:port]/name[:tag|@digest]); update the function that constructs
dockerfileContents to call this validator (referencing sanitizedImage,
dockerfileContents, and buildTarget) and return an error if validation fails so
only safe image references are embedded into the generated Dockerfile.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/devcontainer/compose.go`:
- Around line 1177-1195: In checkForPersistedFile(files []string, prefix string)
change the prefix comparison to operate on the filename, not the full path: use
filepath.Base(file) instead of file when calling strings.HasPrefix so checks
like strings.HasPrefix(filepath.Base(file), prefix) will correctly match entries
in the configFiles list; keep the rest of the logic (os.Stat, os.IsNotExist, and
returned persistedFileResult) unchanged.

---

Outside diff comments:
In `@pkg/devcontainer/build.go`:
- Around line 509-516: The returned config.BuildInfo currently uses
extendResult.buildImageName for ImageName which can be empty; change it to use
the resolved currentImageName (the value chosen between
extendResult.buildImageName and originalImageName) so ImageName is always
populated—update the BuildInfo creation in the function that returns
&config.BuildInfo{...} to set ImageName: currentImageName instead of
extendResult.buildImageName.

---

Nitpick comments:
In `@pkg/devcontainer/compose.go`:
- Around line 672-677: The code currently strips newlines from
composeService.Image and writes it into dockerfileContents using buildTarget,
but it lacks stricter validation; add an image-reference validation step after
computing sanitizedImage (before using it in dockerfileContents) that rejects or
errors on values containing spaces, shell metacharacters (e.g., ; && | ` $ <>),
control characters, or that do not match a Docker image reference pattern (e.g.,
optional domain[:port]/name[:tag|@digest]); update the function that constructs
dockerfileContents to call this validator (referencing sanitizedImage,
dockerfileContents, and buildTarget) and return an error if validation fails so
only safe image references are embedded into the generated Dockerfile.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d3f87ab3-865f-446d-8a1e-cba2e3bf8770

📥 Commits

Reviewing files that changed from the base of the PR and between 5e5a300 and c4b5ac8.

📒 Files selected for processing (5)
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/devcontainer.json
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/devcontainer.json
  • pkg/devcontainer/build.go
  • pkg/devcontainer/compose.go
  • pkg/devcontainer/compose_test.go
✅ Files skipped from review due to trivial changes (1)
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/devcontainer.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/devcontainer.json

The config_files label stores absolute paths, but the prefix check
compared against the full path, so it never matched the filename
prefix (e.g. "docker-compose.devcontainer.build").
@coderabbitai coderabbitai bot added the size/l label Apr 5, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pkg/devcontainer/compose.go (2)

565-606: ⚠️ Potential issue | 🔴 Critical

Preserve the original Dockerfile when build.target is already set.

On the originalTarget != "" path, dockerfileContents stays empty. That sends feature builds into the fallback at Lines 666-677, which synthesizes FROM <composeService.Image> AS <target>. For a valid compose service that has build.target but no image, that becomes FROM AS ... and the feature build fails.

Suggested fix
 		originalTarget := composeService.Build.Target
 		if originalTarget != "" {
 			buildTarget = originalTarget
+			dockerfileContents = string(originalDockerfile)
 		} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainer/compose.go` around lines 565 - 606, When
composeService.Build.Target (originalTarget) is non-empty we currently set
buildTarget but leave dockerfileContents empty, which later causes a synthesized
invalid FROM line; fix this by preserving the original Dockerfile in that
branch: set dockerfileContents = string(originalDockerfile) when originalTarget
!= "" so the subsequent call to getImageBuildInfoFromDockerfile and the returned
composeBuildInfo have the actual Dockerfile; ensure you still set buildTarget =
originalTarget and keep the existing getImageBuildInfoFromDockerfile call
parameters (subCtx, string(originalDockerfile),
mappingToMap(composeService.Build.Args), originalTarget).

448-488: ⚠️ Potential issue | 🟠 Major

Normalize digest-backed refs before inspect/up.

createComposeService strips digests at Line 913 before assigning service.Image, but this block still carries extendResult.buildImageName forward verbatim. With image: repo/app@sha256:..., the feature build is tagged as repo/app while Lines 453 and 487 still inspect/reference repo/app@sha256:..., so the post-build metadata lookup and compose override can target the wrong image.

Suggested fix
-		currentImageName := extendResult.buildImageName
+		overrideImageName := extendResult.buildImageName
+		if extendResult.composeBuildFilePath != "" {
+			overrideImageName = stripDigestFromImageRef(overrideImageName)
+		}
+		currentImageName := overrideImageName
 		if currentImageName == "" {
 			currentImageName = originalImageName
 		}
@@
-			extendResult.buildImageName,
+			overrideImageName,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainer/compose.go` around lines 448 - 488, The code is still using
digest-backed refs (extendResult.buildImageName and originalImageName) when
calling r.inspectImage and r.extendedDockerComposeUp which causes mismatched
lookups; update this block to normalize/strip any digest from
extendResult.buildImageName and originalImageName (the same normalization logic
used in createComposeService for service.Image) before passing them to
r.inspectImage and r.extendedDockerComposeUp so both the inspectImage call and
the compose override use the digest-stripped image name; ensure you apply the
normalization to the variables used in the imageDetails lookup and in the call
to r.extendedDockerComposeUp (references: extendResult.buildImageName,
originalImageName, r.inspectImage, r.extendedDockerComposeUp,
createComposeService).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@pkg/devcontainer/compose.go`:
- Around line 565-606: When composeService.Build.Target (originalTarget) is
non-empty we currently set buildTarget but leave dockerfileContents empty, which
later causes a synthesized invalid FROM line; fix this by preserving the
original Dockerfile in that branch: set dockerfileContents =
string(originalDockerfile) when originalTarget != "" so the subsequent call to
getImageBuildInfoFromDockerfile and the returned composeBuildInfo have the
actual Dockerfile; ensure you still set buildTarget = originalTarget and keep
the existing getImageBuildInfoFromDockerfile call parameters (subCtx,
string(originalDockerfile), mappingToMap(composeService.Build.Args),
originalTarget).
- Around line 448-488: The code is still using digest-backed refs
(extendResult.buildImageName and originalImageName) when calling r.inspectImage
and r.extendedDockerComposeUp which causes mismatched lookups; update this block
to normalize/strip any digest from extendResult.buildImageName and
originalImageName (the same normalization logic used in createComposeService for
service.Image) before passing them to r.inspectImage and
r.extendedDockerComposeUp so both the inspectImage call and the compose override
use the digest-stripped image name; ensure you apply the normalization to the
variables used in the imageDetails lookup and in the call to
r.extendedDockerComposeUp (references: extendResult.buildImageName,
originalImageName, r.inspectImage, r.extendedDockerComposeUp,
createComposeService).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 763fb83b-e511-4677-82db-3f0d47ec3053

📥 Commits

Reviewing files that changed from the base of the PR and between 5e5a300 and 7411f3b.

📒 Files selected for processing (5)
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/devcontainer.json
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/devcontainer.json
  • pkg/devcontainer/build.go
  • pkg/devcontainer/compose.go
  • pkg/devcontainer/compose_test.go
✅ Files skipped from review due to trivial changes (2)
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-b/.devcontainer/devcontainer.json
  • pkg/devcontainer/build.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/tests/up-docker-compose/testdata/docker-compose-features-shared-image-a/.devcontainer/devcontainer.json

@skevetter skevetter merged commit ba33e78 into skevetter:main Apr 6, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compose devcontainers with image: + Features overwrite the shared local image tag

3 participants