Skip to content

Commit e7e269c

Browse files
authored
Merge branch 'main' into dev/Mathi/Issue1069
2 parents 2b01a90 + f27ecff commit e7e269c

16 files changed

Lines changed: 714 additions & 51 deletions
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Docker v20 Tests for dockerfile frontend test
2+
3+
on:
4+
push:
5+
branches: ['main', 'directive-syntax-further-changes']
6+
pull_request:
7+
branches: ['main']
8+
9+
jobs:
10+
test-docker-v20:
11+
name: Docker v20.10 Compatibility
12+
runs-on: ubuntu-22.04
13+
14+
steps:
15+
- uses: actions/checkout@v6
16+
17+
- uses: actions/setup-node@v5
18+
with:
19+
node-version: '18.x'
20+
21+
- name: Install Docker v20.10
22+
run: |
23+
sudo apt-get remove -y docker-ce docker-ce-cli containerd.io || true
24+
sudo apt-get update
25+
sudo apt-get install -y \
26+
ca-certificates \
27+
curl \
28+
gnupg \
29+
lsb-release
30+
sudo mkdir -p /etc/apt/keyrings
31+
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
32+
echo \
33+
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
34+
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
35+
sudo apt-get update
36+
sudo apt-get install -y docker-ce=5:20.10.* docker-ce-cli=5:20.10.* containerd.io
37+
sudo systemctl restart docker
38+
39+
- name: Verify Docker version, Install and Test
40+
run: |
41+
# Verify
42+
docker version
43+
DOCKER_VERSION=$(docker version --format '{{.Server.Version}}')
44+
if [[ ! "$DOCKER_VERSION" =~ ^20\.10\. ]]; then
45+
echo "ERROR: Expected Docker v20.10.x but got $DOCKER_VERSION"
46+
exit 1
47+
fi
48+
yarn install --frozen-lockfile
49+
yarn type-check
50+
yarn package
51+
yarn test-matrix --forbid-only src/test/cli.up.test.ts
52+
env:
53+
CI: true

CHANGELOG.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,22 @@
22

33
Notable changes.
44

5-
## December 2025
5+
## January 2026
66

7-
### [0.81.0]
7+
### [0.82.0]
88
- devcontainer commands now use current directory as default workspace folder when not specified (https://github.com/devcontainers/cli/pull/1104)
99

10+
### [0.81.1]
11+
- Update js-yaml and glob dependencies. (https://github.com/devcontainers/cli/pull/1128)
12+
13+
### [0.81.0]
14+
- Add option to mount a worktree's common folder. (https://github.com/devcontainers/cli/pull/1127)
15+
16+
## December 2025
17+
18+
### [0.80.3]
19+
- Fix: Skip download and injection of `dockerfile:1.4` syntax for Docker Engine versions [>=23.0.0](https://docs.docker.com/engine/release-notes/23.0/#2300)) - `dockerfile:1.4` or a subsequent version is already used by the docker engine package. (https://github.com/devcontainers/cli/pull/1113)
20+
1021
## November 2025
1122

1223
### [0.80.2]

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@devcontainers/cli",
33
"description": "Dev Containers CLI",
4-
"version": "0.81.0",
4+
"version": "0.82.0",
55
"bin": {
66
"devcontainer": "devcontainer.js"
77
},

src/spec-node/configContainer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ async function resolveWithLocalFolder(params: DockerResolverParameters, parsedAu
4646
? (await getDevContainerConfigPathIn(cliHost, workspace.configFolderPath)
4747
|| (overrideConfigFile ? getDefaultDevContainerConfigPath(cliHost, workspace.configFolderPath) : undefined))
4848
: overrideConfigFile;
49-
const configs = configPath && await readDevContainerConfigFile(cliHost, workspace, configPath, params.mountWorkspaceGitRoot, output, workspaceMountConsistencyDefault, overrideConfigFile) || undefined;
49+
const configs = configPath && await readDevContainerConfigFile(cliHost, workspace, configPath, params.mountWorkspaceGitRoot, params.mountGitWorktreeCommonDir, output, workspaceMountConsistencyDefault, overrideConfigFile) || undefined;
5050
if (!configs) {
5151
if (configPath || workspace) {
5252
throw new ContainerError({ description: `Dev container config (${uriToFsPath(configPath || getDefaultDevContainerConfigPath(cliHost, workspace!.configFolderPath), cliHost.platform)}) not found.` });
@@ -79,7 +79,7 @@ async function resolveWithLocalFolder(params: DockerResolverParameters, parsedAu
7979
return result;
8080
}
8181

82-
export async function readDevContainerConfigFile(cliHost: CLIHost, workspace: Workspace | undefined, configFile: URI, mountWorkspaceGitRoot: boolean, output: Log, consistency?: BindMountConsistency, overrideConfigFile?: URI) {
82+
export async function readDevContainerConfigFile(cliHost: CLIHost, workspace: Workspace | undefined, configFile: URI, mountWorkspaceGitRoot: boolean, mountGitWorktreeCommonDir: boolean, output: Log, consistency?: BindMountConsistency, overrideConfigFile?: URI) {
8383
const documents = createDocuments(cliHost);
8484
const content = await documents.readDocument(overrideConfigFile ?? configFile);
8585
if (!content) {
@@ -90,7 +90,7 @@ export async function readDevContainerConfigFile(cliHost: CLIHost, workspace: Wo
9090
if (!updated || typeof updated !== 'object' || Array.isArray(updated)) {
9191
throw new ContainerError({ description: `Dev container config (${uriToFsPath(configFile, cliHost.platform)}) must contain a JSON object literal.` });
9292
}
93-
const workspaceConfig = await getWorkspaceConfiguration(cliHost, workspace, updated, mountWorkspaceGitRoot, output, consistency);
93+
const workspaceConfig = await getWorkspaceConfiguration(cliHost, workspace, updated, mountWorkspaceGitRoot, mountGitWorktreeCommonDir, output, consistency);
9494
const substitute0: SubstituteConfig = value => substitute({
9595
platform: cliHost.platform,
9696
localWorkspaceFolder: workspace?.rootFolderPath,

src/spec-node/containerFeatures.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export async function getExtendImageBuildInfo(params: DockerResolverParameters,
154154
}
155155
};
156156
}
157-
return { featureBuildInfo: getImageBuildOptions(params, config, dstFolder, baseName, imageBuildInfo) };
157+
return { featureBuildInfo: await getImageBuildOptions(params, config, dstFolder, baseName, imageBuildInfo) };
158158
}
159159

160160
// Generates the end configuration.
@@ -193,24 +193,24 @@ export interface ImageBuildOptions {
193193
securityOpts: string[];
194194
}
195195

196-
function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig<DevContainerConfig>, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): ImageBuildOptions {
197-
const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax;
198-
return {
199-
dstFolder,
200-
dockerfileContent: `
196+
async function getImageBuildOptions(params: DockerResolverParameters, config: SubstitutedConfig<DevContainerConfig>, dstFolder: string, baseName: string, imageBuildInfo: ImageBuildInfo): Promise<ImageBuildOptions> {
197+
const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax;
198+
return {
199+
dstFolder,
200+
dockerfileContent: `
201201
FROM $_DEV_CONTAINERS_BASE_IMAGE AS dev_containers_target_stage
202202
${getDevcontainerMetadataLabel(getDevcontainerMetadata(imageBuildInfo.metadata, config, { featureSets: [] }, [], getOmitDevcontainerPropertyOverride(params.common)))}
203203
`,
204-
overrideTarget: 'dev_containers_target_stage',
205-
dockerfilePrefixContent: `${syntax ? `# syntax=${syntax}` : ''}
206-
ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder
204+
overrideTarget: 'dev_containers_target_stage',
205+
dockerfilePrefixContent: `${syntax ? `# syntax=${syntax}` : ''}
206+
ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder
207207
`,
208-
buildArgs: {
209-
_DEV_CONTAINERS_BASE_IMAGE: baseName,
210-
} as Record<string, string>,
211-
buildKitContexts: {} as Record<string, string>,
212-
securityOpts: [],
213-
};
208+
buildArgs: {
209+
_DEV_CONTAINERS_BASE_IMAGE: baseName,
210+
} as Record<string, string>,
211+
buildKitContexts: {} as Record<string, string>,
212+
securityOpts: [],
213+
};
214214
}
215215

216216
function getOmitDevcontainerPropertyOverride(resolverParams: { omitConfigRemotEnvFromMetadata?: boolean }): (keyof DevContainerConfig & keyof ImageMetadataEntry)[] {
@@ -241,7 +241,10 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont
241241
const useBuildKitBuildContexts = buildKitVersionParsed ? !isEarlierVersion(buildKitVersionParsed, minRequiredVersion) : false;
242242
const buildContentImageName = 'dev_container_feature_content_temp';
243243
const disableSELinuxLabels = useBuildKitBuildContexts && await isUsingSELinuxLabels(params);
244-
244+
// Access Docker engine version
245+
const dockerEngineVersionParsed = params.dockerEngineVersion?.versionMatch ? parseVersion(params.dockerEngineVersion.versionMatch) : undefined;
246+
const minDockerEngineVersion = [23, 0, 0];
247+
const skipDefaultSyntax = dockerEngineVersionParsed ? !isEarlierVersion(dockerEngineVersionParsed, minDockerEngineVersion) : false;
245248
const omitPropertyOverride = params.common.skipPersistingCustomizationsFromFeatures ? ['customizations'] : [];
246249
const imageMetadata = getDevcontainerMetadata(imageBuildInfo.metadata, devContainerConfig, featuresConfig, omitPropertyOverride, getOmitDevcontainerPropertyOverride(params.common));
247250
const { containerUser, remoteUser } = findContainerUsers(imageMetadata, composeServiceUser, imageBuildInfo.user);
@@ -262,11 +265,12 @@ async function getFeaturesBuildOptions(params: DockerResolverParameters, devCont
262265
.replace('#{devcontainerMetadata}', getDevcontainerMetadataLabel(imageMetadata))
263266
.replace('#{containerEnvMetadata}', generateContainerEnvs(devContainerConfig.config.containerEnv, true))
264267
;
265-
const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax;
266-
const omitSyntaxDirective = common.omitSyntaxDirective; // Can be removed when https://github.com/moby/buildkit/issues/4556 is fixed
267-
const dockerfilePrefixContent = `${omitSyntaxDirective ? '' :
268-
useBuildKitBuildContexts && !(imageBuildInfo.dockerfile && supportsBuildContexts(imageBuildInfo.dockerfile)) ? '# syntax=docker/dockerfile:1.4' :
269-
syntax ? `# syntax=${syntax}` : ''}
268+
const syntax = imageBuildInfo.dockerfile?.preamble.directives.syntax;
269+
const omitSyntaxDirective = common.omitSyntaxDirective; // Can be removed when https://github.com/moby/buildkit/issues/4556 is fixed
270+
const dockerfilePrefixContent = `${omitSyntaxDirective ? '' :
271+
skipDefaultSyntax ? (syntax ? `# syntax=${syntax}` : '') :
272+
useBuildKitBuildContexts && !(imageBuildInfo.dockerfile && supportsBuildContexts(imageBuildInfo.dockerfile)) ? '# syntax=docker/dockerfile:1.4' :
273+
syntax ? `# syntax=${syntax}` : ''}
270274
ARG _DEV_CONTAINERS_BASE_IMAGE=placeholder
271275
`;
272276

src/spec-node/devContainers.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { LogLevel, LogDimensions, toErrorText, createCombinedLog, createTerminal
1717
import { dockerComposeCLIConfig } from './dockerCompose';
1818
import { Mount } from '../spec-configuration/containerFeaturesConfiguration';
1919
import { getPackageConfig, PackageConfiguration } from '../spec-utils/product';
20-
import { dockerBuildKitVersion, isPodman } from '../spec-shutdown/dockerUtils';
20+
import { dockerBuildKitVersion, dockerEngineVersion, isPodman } from '../spec-shutdown/dockerUtils';
2121
import { Event } from '../spec-utils/event';
2222

2323

@@ -30,6 +30,7 @@ export interface ProvisionOptions {
3030
workspaceMountConsistency?: BindMountConsistency;
3131
gpuAvailability?: GPUAvailability;
3232
mountWorkspaceGitRoot: boolean;
33+
mountGitWorktreeCommonDir: boolean;
3334
configFile: URI | undefined;
3435
overrideConfigFile: URI | undefined;
3536
logLevel: LogLevel;
@@ -102,7 +103,7 @@ export async function launch(options: ProvisionOptions, providedIdLabels: string
102103
}
103104

104105
export async function createDockerParams(options: ProvisionOptions, disposables: (() => Promise<unknown> | undefined)[]): Promise<DockerResolverParameters> {
105-
const { persistedFolder, additionalMounts, updateRemoteUserUIDDefault, containerDataFolder, containerSystemDataFolder, workspaceMountConsistency, gpuAvailability, mountWorkspaceGitRoot, remoteEnv, experimentalLockfile, experimentalFrozenLockfile, omitLoggerHeader, secretsP } = options;
106+
const { persistedFolder, additionalMounts, updateRemoteUserUIDDefault, containerDataFolder, containerSystemDataFolder, workspaceMountConsistency, gpuAvailability, mountWorkspaceGitRoot, mountGitWorktreeCommonDir, remoteEnv, experimentalLockfile, experimentalFrozenLockfile, omitLoggerHeader, secretsP } = options;
106107
let parsedAuthority: DevContainerAuthority | undefined;
107108
if (options.workspaceFolder) {
108109
parsedAuthority = { hostPath: options.workspaceFolder } as DevContainerAuthority;
@@ -205,6 +206,16 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
205206
output,
206207
platformInfo
207208
}));
209+
210+
const dockerEngineVer = await dockerEngineVersion({
211+
cliHost,
212+
dockerCLI: dockerPath,
213+
dockerComposeCLI,
214+
env: cliHost.env,
215+
output,
216+
platformInfo
217+
});
218+
208219
return {
209220
common,
210221
parsedAuthority,
@@ -215,6 +226,7 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
215226
workspaceMountConsistencyDefault: workspaceMountConsistency,
216227
gpuAvailability: gpuAvailability || 'detect',
217228
mountWorkspaceGitRoot,
229+
mountGitWorktreeCommonDir,
218230
updateRemoteUserUIDOnMacOS: false,
219231
cacheMount: 'bind',
220232
removeOnStartup: options.removeExistingContainer,
@@ -225,6 +237,7 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
225237
updateRemoteUserUIDDefault,
226238
additionalCacheFroms: options.additionalCacheFroms,
227239
buildKitVersion,
240+
dockerEngineVersion: dockerEngineVer,
228241
isTTY: process.stdout.isTTY || options.logFormat === 'json',
229242
experimentalLockfile,
230243
experimentalFrozenLockfile,

0 commit comments

Comments
 (0)