From f4f3490442bd584b1b40272ea47570837adb7fc5 Mon Sep 17 00:00:00 2001 From: Sandro Meier Date: Thu, 9 Apr 2026 22:19:44 +0200 Subject: [PATCH 1/3] fix: always write devcontainer.metadata label as JSON array When there is only one metadata entry (e.g. docker-compose devcontainer with no features), `getDevcontainerMetadataLabel` wrote a bare JSON object instead of an array. This violates the spec which states the label "can contain an array of json snippets" and causes tools like Zed that expect an array to fail when attaching to an existing container. Always wrap the metadata in an array regardless of the number of entries. Spec reference: https://containers.dev/implementors/json_reference/ Fixes #1054 --- src/spec-node/imageMetadata.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/spec-node/imageMetadata.ts b/src/spec-node/imageMetadata.ts index 70a913965..60884592e 100644 --- a/src/spec-node/imageMetadata.ts +++ b/src/spec-node/imageMetadata.ts @@ -497,11 +497,9 @@ export function getDevcontainerMetadataLabel(devContainerMetadata: SubstitutedCo if (!metadata.length) { return ''; } - const imageMetadataLabelValue = metadata.length !== 1 - ? `[${metadata - .map(feature => ` \\\n${toLabelString(feature)}`) - .join(',')} \\\n]` - : toLabelString(metadata[0]); + const imageMetadataLabelValue = `[${metadata + .map(feature => ` \\\n${toLabelString(feature)}`) + .join(',')} \\\n]`; return `LABEL ${imageMetadataLabel}="${imageMetadataLabelValue}"`; } From 1df0782ec8b5fd93d7f2fab4267df0c43ff1c91c Mon Sep 17 00:00:00 2001 From: Sandro Meier Date: Thu, 9 Apr 2026 22:35:40 +0200 Subject: [PATCH 2/3] cleanup: remove unnecessary spaces --- src/spec-node/imageMetadata.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spec-node/imageMetadata.ts b/src/spec-node/imageMetadata.ts index 60884592e..51c129338 100644 --- a/src/spec-node/imageMetadata.ts +++ b/src/spec-node/imageMetadata.ts @@ -498,8 +498,8 @@ export function getDevcontainerMetadataLabel(devContainerMetadata: SubstitutedCo return ''; } const imageMetadataLabelValue = `[${metadata - .map(feature => ` \\\n${toLabelString(feature)}`) - .join(',')} \\\n]`; + .map(feature => `\\\n${toLabelString(feature)}`) + .join(',')}\\\n]`; return `LABEL ${imageMetadataLabel}="${imageMetadataLabelValue}"`; } From 3d19facdffd7c0a190ce9fa7ae7004a8dd63cc35 Mon Sep 17 00:00:00 2001 From: Sandro Meier Date: Thu, 9 Apr 2026 23:01:45 +0200 Subject: [PATCH 3/3] test: add test for single metadata entry always being an array --- src/spec-node/imageMetadata.ts | 4 ++-- src/test/imageMetadata.test.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/spec-node/imageMetadata.ts b/src/spec-node/imageMetadata.ts index 51c129338..60884592e 100644 --- a/src/spec-node/imageMetadata.ts +++ b/src/spec-node/imageMetadata.ts @@ -498,8 +498,8 @@ export function getDevcontainerMetadataLabel(devContainerMetadata: SubstitutedCo return ''; } const imageMetadataLabelValue = `[${metadata - .map(feature => `\\\n${toLabelString(feature)}`) - .join(',')}\\\n]`; + .map(feature => ` \\\n${toLabelString(feature)}`) + .join(',')} \\\n]`; return `LABEL ${imageMetadataLabel}="${imageMetadataLabelValue}"`; } diff --git a/src/test/imageMetadata.test.ts b/src/test/imageMetadata.test.ts index 24045ac4e..fb78df0f5 100644 --- a/src/test/imageMetadata.test.ts +++ b/src/test/imageMetadata.test.ts @@ -432,6 +432,22 @@ describe('Image Metadata', function () { assert.strictEqual(label.replace(/ \\\n/g, ''), `LABEL devcontainer.metadata="${JSON.stringify(expected).replace(/"/g, '\\"')}"`); }); + it('should create array label for single metadata entry (docker-compose with Dockerfile, no features)', () => { + // When there is only one metadata entry, the label should still be a JSON array. + // Regression test for https://github.com/devcontainers/cli/issues/1054 + const label = getDevcontainerMetadataLabel(configWithRaw([ + { + remoteUser: 'testUser', + } + ])); + const expected = [ + { + remoteUser: 'testUser', + } + ]; + assert.strictEqual(label.replace(/ \\\n/g, ''), `LABEL devcontainer.metadata="${JSON.stringify(expected).replace(/"/g, '\\"')}"`); + }); + it('should merge metadata from devcontainer.json and features', () => { const merged = mergeConfiguration({ configFilePath: URI.parse('file:///devcontainer.json'),