Skip to content

Commit 805196f

Browse files
authored
Merge branch 'main' into dev/Mathi/CliPd
2 parents 49aef84 + 65f98a5 commit 805196f

20 files changed

Lines changed: 289 additions & 53 deletions

.devcontainer/devcontainer-lock.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
"integrity": "sha256:ce078b7bf7d9ef3bcb9813b32103795d8d72172446890b64772cbe1dec6baafd"
77
}
88
}
9-
}
9+
}

.devcontainer/devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"vscode": {
2828
"extensions": [
2929
"dbaeumer.vscode-eslint",
30-
"GitHub.vscode-pull-request-github"
30+
"GitHub.vscode-pull-request-github",
31+
"hbenl.vscode-mocha-test-adapter"
3132
]
3233
},
3334
"codespaces": {

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Notable changes.
44

55
## May 2026
66

7+
### [0.87.0]
8+
- Graduate lockfile from experimental to stable: lockfiles are now generated by default on `build` and `up`. (https://github.com/devcontainers/cli/issues/1195)
9+
- New `--no-lockfile` flag to opt out of lockfile generation.
10+
- New `--frozen-lockfile` flag to ensure the lockfile exists and remains unchanged.
11+
- `--experimental-lockfile` and `--experimental-frozen-lockfile` are deprecated (still accepted with a warning).
12+
713
### [0.86.1]
814
- Do not write features supplied via `--additional-features` to the lockfile. (https://github.com/microsoft/vscode-remote-release/issues/11616)
915

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@ This CLI is in active development. Current status:
1515
- [x] `devcontainer run-user-commands` - Runs lifecycle commands like `postCreateCommand`
1616
- [x] `devcontainer read-configuration` - Outputs current configuration for workspace
1717
- [x] `devcontainer exec` - Executes a command in a container with `userEnvProbe`, `remoteUser`, `remoteEnv`, and other properties applied
18+
- [x] `devcontainer outdated` - Show outdated lockfile features
19+
- [x] `devcontainer upgrade` - Upgrade lockfile features
1820
- [x] `devcontainer features <...>` - Tools to assist in authoring and testing [Dev Container Features](https://containers.dev/implementors/features/)
1921
- [x] `devcontainer templates <...>` - Tools to assist in authoring and testing [Dev Container Templates](https://containers.dev/implementors/templates/)
2022
- [ ] `devcontainer stop` - Stops containers
2123
- [ ] `devcontainer down` - Stops and deletes containers
2224

25+
Lockfiles (`.devcontainer-lock.json`) are generated by default when running `build` or `up` to pin feature versions for reproducible builds. Use `--no-lockfile` to opt out, or `--frozen-lockfile` to enforce an existing lockfile.
26+
2327
## Try it out
2428

2529
We'd love for you to try out the dev container CLI and let us know what you think. You can quickly try it out in just a few simple steps, either by using the install script, installing its npm package, or building the CLI repo from sources (see "[Build from sources](#build-from-sources)").

docs/contributing-code.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ node devcontainer.js run-user-commands --workspace-folder <path>
9595

9696
Tests use [Mocha](https://mochajs.org/) and [Chai](https://www.chaijs.com/) and require Docker because they create and tear down real containers.
9797

98+
Before running tests, package the CLI into a tarball:
99+
100+
```sh
101+
npm run package
102+
```
103+
104+
Tests install the CLI from the generated `devcontainers-cli-<version>.tgz` and shell out to it as a subprocess. You must re-run `npm run package` after any code change so that the tarball reflects your latest changes. Running `npm run compile` alone is **not** sufficient — it builds the JavaScript output but does not create the tarball that the tests depend on.
105+
98106
```sh
99107
npm test # all tests
100108
npm run test-container-features # Features tests only

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.86.1",
4+
"version": "0.87.0",
55
"bin": {
66
"devcontainer": "devcontainer.js"
77
},

src/spec-configuration/containerFeaturesConfiguration.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ export interface ContainerFeatureInternalParams {
193193
env: NodeJS.ProcessEnv;
194194
skipFeatureAutoMapping: boolean;
195195
platform: NodeJS.Platform;
196-
experimentalLockfile?: boolean;
197-
experimentalFrozenLockfile?: boolean;
196+
noLockfile?: boolean;
197+
frozenLockfile?: boolean;
198198
}
199199

200200
// TODO: Move to node layer.
@@ -485,7 +485,7 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar
485485

486486
const ociCacheDir = await prepareOCICache(dstFolder);
487487

488-
const { lockfile, initLockfile } = await readLockfile(config);
488+
const { lockfile } = params.noLockfile ? { lockfile: undefined } : await readLockfile(config);
489489

490490
const processFeature = async (_userFeature: DevContainerFeature) => {
491491
return await processFeatureIdentifier(params, configPath, workspaceRoot, _userFeature, lockfile);
@@ -508,7 +508,9 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar
508508
await fetchFeatures(params, featuresConfig, dstFolder, ociCacheDir, lockfile);
509509

510510
await logFeatureAdvisories(params, featuresConfig);
511-
await writeLockfile(params, config, await generateLockfile(featuresConfig, config, additionalFeatures), initLockfile);
511+
if (!params.noLockfile) {
512+
await writeLockfile(params, config, await generateLockfile(featuresConfig, config, additionalFeatures));
513+
}
512514
return featuresConfig;
513515
}
514516

src/spec-configuration/lockfile.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ export async function generateLockfile(featuresConfig: FeaturesConfig, config?:
4545
});
4646
}
4747

48-
export async function writeLockfile(params: ContainerFeatureInternalParams, config: DevContainerConfig, lockfile: Lockfile, forceInitLockfile?: boolean): Promise<string | undefined> {
48+
export async function writeLockfile(params: ContainerFeatureInternalParams, config: DevContainerConfig, lockfile: Lockfile): Promise<string | undefined> {
49+
if (params.noLockfile) {
50+
return;
51+
}
52+
4953
const lockfilePath = getLockfilePath(config);
5054
const oldLockfileContent = await readLocalFile(lockfilePath)
5155
.catch(err => {
@@ -54,14 +58,10 @@ export async function writeLockfile(params: ContainerFeatureInternalParams, conf
5458
}
5559
});
5660

57-
if (!forceInitLockfile && !oldLockfileContent && !params.experimentalLockfile && !params.experimentalFrozenLockfile) {
58-
return;
59-
}
60-
6161
// Trailing newline per POSIX convention
6262
const newLockfileContentString = JSON.stringify(lockfile, null, 2) + '\n';
6363
const newLockfileContent = Buffer.from(newLockfileContentString);
64-
if (params.experimentalFrozenLockfile && !oldLockfileContent) {
64+
if (params.frozenLockfile && !oldLockfileContent) {
6565
throw new Error('Lockfile does not exist.');
6666
}
6767
// Normalize the existing lockfile through JSON.parse -> JSON.stringify to produce
@@ -76,7 +76,7 @@ export async function writeLockfile(params: ContainerFeatureInternalParams, conf
7676
}
7777
}
7878
if (!oldLockfileNormalized || oldLockfileNormalized !== newLockfileContentString) {
79-
if (params.experimentalFrozenLockfile) {
79+
if (params.frozenLockfile) {
8080
throw new Error('Lockfile does not match.');
8181
}
8282
await writeLocalFile(lockfilePath, newLockfileContent);

src/spec-node/containerFeatures.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@ export async function getExtendImageBuildInfo(params: DockerResolverParameters,
147147
const platform = params.common.cliHost.platform;
148148

149149
const cacheFolder = await getCacheFolder(params.common.cliHost);
150-
const { experimentalLockfile, experimentalFrozenLockfile } = params;
151-
const featuresConfig = await generateFeaturesConfig({ ...params.common, platform, cacheFolder, experimentalLockfile, experimentalFrozenLockfile }, dstFolder, config.config, additionalFeatures);
150+
const { noLockfile, frozenLockfile } = params;
151+
const featuresConfig = await generateFeaturesConfig({ ...params.common, platform, cacheFolder, noLockfile, frozenLockfile }, dstFolder, config.config, additionalFeatures);
152152
if (!featuresConfig) {
153153
if (canAddLabelsToContainer && !imageBuildInfo.dockerfile) {
154154
return {

src/spec-node/devContainers.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ export interface ProvisionOptions {
6868
installCommand?: string;
6969
targetPath?: string;
7070
};
71-
experimentalLockfile?: boolean;
72-
experimentalFrozenLockfile?: boolean;
71+
noLockfile?: boolean;
72+
frozenLockfile?: boolean;
7373
secretsP?: Promise<Record<string, string>>;
7474
omitSyntaxDirective?: boolean;
7575
includeConfig?: boolean;
@@ -103,7 +103,7 @@ export async function launch(options: ProvisionOptions, providedIdLabels: string
103103
}
104104

105105
export async function createDockerParams(options: ProvisionOptions, disposables: (() => Promise<unknown> | undefined)[]): Promise<DockerResolverParameters> {
106-
const { persistedFolder, additionalMounts, updateRemoteUserUIDDefault, containerDataFolder, containerSystemDataFolder, workspaceMountConsistency, gpuAvailability, mountWorkspaceGitRoot, mountGitWorktreeCommonDir, remoteEnv, experimentalLockfile, experimentalFrozenLockfile, omitLoggerHeader, secretsP } = options;
106+
const { persistedFolder, additionalMounts, updateRemoteUserUIDDefault, containerDataFolder, containerSystemDataFolder, workspaceMountConsistency, gpuAvailability, mountWorkspaceGitRoot, mountGitWorktreeCommonDir, remoteEnv, noLockfile, frozenLockfile, omitLoggerHeader, secretsP } = options;
107107
let parsedAuthority: DevContainerAuthority | undefined;
108108
if (options.workspaceFolder) {
109109
parsedAuthority = { hostPath: options.workspaceFolder } as DevContainerAuthority;
@@ -246,8 +246,8 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
246246
buildKitVersion,
247247
dockerEngineVersion: dockerEngineVer,
248248
isTTY: process.stdout.isTTY || options.logFormat === 'json',
249-
experimentalLockfile,
250-
experimentalFrozenLockfile,
249+
noLockfile,
250+
frozenLockfile,
251251
buildxPlatform: common.buildxPlatform,
252252
buildxPush: common.buildxPush,
253253
additionalLabels: options.additionalLabels,

0 commit comments

Comments
 (0)