Skip to content

fix(devcontainer): keep compose metadata label valid JSON#771

Open
YassineElbouchaibi wants to merge 2 commits into
skevetter:mainfrom
YassineElbouchaibi:fix/compose-metadata-label-single-quote-escaping
Open

fix(devcontainer): keep compose metadata label valid JSON#771
YassineElbouchaibi wants to merge 2 commits into
skevetter:mainfrom
YassineElbouchaibi:fix/compose-metadata-label-single-quote-escaping

Conversation

@YassineElbouchaibi

@YassineElbouchaibi YassineElbouchaibi commented Jun 26, 2026

Copy link
Copy Markdown

Summary

Fixes #770.

On Docker Compose–based devcontainers, customizations.vscode.extensions/settings were silently dropped whenever a lifecycle command contained single quotes (e.g. a postStartCommand using a single-quoted grep regex).

Root cause: when serializing additional labels into the generated docker-compose override file, generateDockerComposeUpProject rewrote every single quote ' in the label value to \'\':

// before
label := regexp.MustCompile(`\$`).ReplaceAllString(v, "$$$$")
label = regexp.MustCompile(`'`).ReplaceAllString(label, `\'\'`)

That injects an illegal \' escape into the devcontainer.metadata JSON label. On the next up/attach, GetImageMetadataFromContainerjson.Unmarshal fails with:

error Error parsing image metadata: invalid character '\'' in string escape code

The error is logged but swallowed, empty metadata is returned, and MergeConfiguration drops all customizations — so extensions/settings never get applied, while the container otherwise comes up fine.

The single-quote rewrite is a mis-port of the devcontainers/cli reference, whose JS replacement '\'\'' evaluates to '' (YAML single-quote doubling), not the literal characters \'\'. Because devpod writes the override file with yaml.Marshal (which already handles all YAML quoting), the manual single-quote escaping is both unnecessary and corrupting.

Change

  • Extract the label escaping into escapeComposeLabelValue, which now only doubles $ so docker compose does not interpolate it ($$$). Single quotes are left untouched and handled by yaml.Marshal.
  • Add a regression test that round-trips a quote-heavy postStartCommand plus a customizations.vscode.extensions entry through the exact pipeline (escape → yaml.Marshal → compose interpolation → json.Unmarshal) and asserts the customizations survive, plus a test confirming literal $ is still preserved for compose.

Verification

$ go test ./pkg/devcontainer/...
ok  	github.com/skevetter/devpod/pkg/devcontainer

Before this change the new TestEscapeComposeLabelValue_PreservesQuoteHeavyMetadata fails with the exact invalid character '\'' in string escape code error; after, it passes and the golang.go extension survives the round-trip.

Notes / follow-up (out of scope here)

This PR fixes the corruption at the source. As defense-in-depth, metadata.GetImageMetadataFromContainer still silently swallows any future label-parse failure (logs and returns empty metadata). A follow-up could surface that loudly and/or fall back to the parsed devcontainer.json customizations. Kept separate to keep this change focused.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of container metadata labels so values with single quotes remain valid through compose processing.
    • Ensured literal $ characters are preserved correctly in label values instead of being treated as variables.
    • Added regression coverage for quote-heavy and dollar-containing metadata to prevent future breakage.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6fc7d3ed-a4ca-4912-a520-ba37c4d7c1f4

📥 Commits

Reviewing files that changed from the base of the PR and between 0d0521d and a19ad04.

📒 Files selected for processing (2)
  • pkg/devcontainer/compose.go
  • pkg/devcontainer/compose_label_escape_test.go

📝 Walkthrough

Walkthrough

Compose devcontainer label generation now escapes only dollar signs before writing additionalLabels. New tests round-trip metadata through YAML and compose interpolation to verify quote-heavy JSON stays valid and literal $ values are preserved.

Changes

Compose label escaping

Layer / File(s) Summary
Label escaping helper
pkg/devcontainer/compose.go
Adds escapeComposeLabelValue to double $ in compose label values, and uses it when adding additionalLabels in generateDockerComposeUpProject.
Round-trip tests
pkg/devcontainer/compose_label_escape_test.go
Adds a helper that runs the metadata label through YAML marshal/unmarshal and compose interpolation, plus regressions for quote-heavy metadata and literal $ values.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing Compose devcontainer metadata label JSON validity.
Linked Issues check ✅ Passed The change removes the bad single-quote escaping, preserves $ escaping, and adds regression tests for the reported Compose metadata bug.
Out of Scope Changes check ✅ Passed The changes stay focused on Compose label escaping and targeted regression tests, with no unrelated scope detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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.

@YassineElbouchaibi YassineElbouchaibi force-pushed the fix/compose-metadata-label-single-quote-escaping branch 2 times, most recently from 2789372 to 5586a66 Compare June 26, 2026 19:55
Quote-heavy lifecycle commands (e.g. a postStartCommand using a
single-quoted grep) caused the generated devcontainer.metadata image
label to contain an illegal `\'` JSON escape, because every single quote
in the label value was rewritten to `\'\'` before yaml.Marshal.

On the next up/attach, GetImageMetadataFromContainer failed to unmarshal
the label, swallowed the error, and returned empty metadata, so all
customizations.vscode.extensions/settings were silently dropped on
Compose-based devcontainers.

yaml.Marshal already handles YAML quoting, so the manual single-quote
escaping is unnecessary and corrupting. Extract the label escaping into
escapeComposeLabelValue, which now only doubles `$` for compose
interpolation, and add a regression test that round-trips a quote-heavy
postStartCommand plus a vscode extension through the compose override.

Fixes skevetter#770

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@YassineElbouchaibi YassineElbouchaibi force-pushed the fix/compose-metadata-label-single-quote-escaping branch from 5586a66 to a19ad04 Compare June 26, 2026 20:34
@YassineElbouchaibi YassineElbouchaibi marked this pull request as ready for review June 26, 2026 21:11
Copilot AI review requested due to automatic review settings June 26, 2026 21:11

Copilot AI left a comment

Copy link
Copy Markdown

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-only devcontainer bug where devcontainer.metadata labels could be corrupted into invalid JSON when lifecycle commands contained single quotes, causing VS Code customizations (extensions/settings) to be silently dropped on attach.

Changes:

  • Extract label escaping into escapeComposeLabelValue, removing the incorrect single-quote rewriting and keeping only $-doubling to prevent Docker Compose interpolation.
  • Add regression tests that round-trip quote-heavy metadata through YAML marshal/unmarshal + Compose interpolation and verify JSON validity and customization preservation.
  • Add a focused test ensuring literal $ survives the Compose interpolation round-trip.

Reviewed changes

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

File Description
pkg/devcontainer/compose.go Replaces the prior regex-based escaping (including broken single-quote escaping) with a dedicated helper that only escapes $ for Compose interpolation safety.
pkg/devcontainer/compose_label_escape_test.go Adds regression coverage that reproduces the prior JSON corruption and verifies both quote preservation and $-escaping behavior.

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

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Compose devcontainer customizations (extensions/settings) silently dropped when devcontainer.metadata label fails to parse

2 participants