Skip to content

Commit c4e9697

Browse files
committed
Use spread semantics for extendsMergeMode override
Override merge now fully replaces each set top-level property from the child config instead of shallow-merging object maps and hostRequirements.
1 parent ff4be89 commit c4e9697

4 files changed

Lines changed: 16 additions & 23 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Notable changes.
44

55
## Unreleased
66

7-
- Add support for the `extends` keyword so one `devcontainer.json` can inherit another, with optional `extendsMergeMode` (`combine` uses image metadata merge logic; `override` replaces arrays/scalars from the child). (https://github.com/devcontainers/spec/issues/22, https://github.com/devcontainers/cli/pull/311)
7+
- Add support for the `extends` keyword so one `devcontainer.json` can inherit another, with optional `extendsMergeMode` (`combine` uses image metadata merge logic; `override` uses `{ ...base, ...overlay }` so each set top-level property from the child fully replaces the inherited value). (https://github.com/devcontainers/spec/issues/22, https://github.com/devcontainers/cli/pull/311)
88

99
## August 2026
1010

‎src/spec-node/imageMetadata.ts‎

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,8 @@ export function mergeDevContainerConfigs(base: DevContainerConfig, overlay: DevC
272272
}
273273

274274
/**
275-
* Overlay-style merge: scalars and arrays from the overlay replace the base when set;
276-
* object maps and `hostRequirements` are shallow-merged with overlay keys winning.
275+
* Overlay-style merge: each top-level property from the overlay replaces the base value when set;
276+
* omitted overlay keys keep the inherited base value ({ ...base, ...overlay }).
277277
*/
278278
function mergeDevContainerConfigsOverride(base: DevContainerConfig, overlay: DevContainerConfig): DevContainerConfig {
279279
const merged = {
@@ -282,20 +282,6 @@ function mergeDevContainerConfigsOverride(base: DevContainerConfig, overlay: Dev
282282
} as DevContainerConfig;
283283
delete merged.extends;
284284
delete merged.extendsMergeMode;
285-
286-
const remoteEnv = Object.assign({}, base.remoteEnv, overlay.remoteEnv);
287-
assignOrDelete(merged, 'remoteEnv', Object.keys(remoteEnv).length ? remoteEnv : undefined);
288-
const containerEnv = Object.assign({}, base.containerEnv, overlay.containerEnv);
289-
assignOrDelete(merged, 'containerEnv', Object.keys(containerEnv).length ? containerEnv : undefined);
290-
const portsAttributes = Object.assign({}, base.portsAttributes, overlay.portsAttributes);
291-
assignOrDelete(merged, 'portsAttributes', Object.keys(portsAttributes).length ? portsAttributes : undefined);
292-
const features = Object.assign({}, base.features, overlay.features);
293-
assignOrDelete(merged, 'features', Object.keys(features).length ? features : undefined);
294-
const customizations = Object.assign({}, base.customizations, overlay.customizations);
295-
assignOrDelete(merged, 'customizations', Object.keys(customizations).length ? customizations : undefined);
296-
const hostRequirements = Object.assign({}, base.hostRequirements, overlay.hostRequirements);
297-
assignOrDelete(merged, 'hostRequirements', Object.keys(hostRequirements).length ? hostRequirements : undefined);
298-
299285
return merged;
300286
}
301287

‎src/test/cli.extends.up.test.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ describe('Dev Containers CLI extends', function () {
6565
containerId = response.containerId;
6666
assert.ok(containerId, 'Container id not found.');
6767
assert.equal(response.configuration?.name, 'extends-up-override');
68-
assert.equal(response.configuration?.remoteEnv?.EXTENDS_UP, 'override');
68+
assert.deepEqual(response.configuration?.remoteEnv, {
69+
OVERRIDE_ME: 'child',
70+
EXTENDS_UP: 'override',
71+
});
6972
assert.deepEqual(response.configuration?.forwardPorts, [9999]);
7073
});
7174

‎src/test/configContainer.test.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,14 @@ describe('readDevContainerConfigFile', function () {
9494
assert.strictEqual(raw.name, 'Override merge');
9595
assert.deepEqual(raw.forwardPorts, [443]);
9696
assert.strictEqual(raw.init, false);
97-
assert.strictEqual(raw.hostRequirements?.cpus, 2);
98-
assert.strictEqual(raw.hostRequirements?.memory, '4gb');
97+
assert.deepEqual(raw.remoteEnv, { OVERRIDE_ME: 'child' });
98+
assert.deepEqual(raw.features, {
99+
'ghcr.io/devcontainers/features/docker-in-docker:1': {
100+
version: 'latest',
101+
moby: true,
102+
},
103+
});
104+
assert.deepEqual(raw.hostRequirements, { memory: '4gb' });
99105
assert.notProperty(raw as any, 'extends');
100106
assert.notProperty(raw as any, 'extendsMergeMode');
101107
});
@@ -182,8 +188,6 @@ describe('mergeDevContainerConfigs', function () {
182188
assert.strictEqual(merged.init, false);
183189
assert.strictEqual(merged.privileged, true);
184190
assert.deepEqual(merged.forwardPorts, [443]);
185-
assert.strictEqual(merged.hostRequirements?.cpus, 4);
186-
assert.strictEqual(merged.hostRequirements?.memory, '4gb');
187-
assert.strictEqual(merged.hostRequirements?.storage, '32gb');
191+
assert.deepEqual(merged.hostRequirements, { memory: '4gb' });
188192
});
189193
});

0 commit comments

Comments
 (0)