From 3c99c5e586c01227f73c7f610ea18d16bd99eed7 Mon Sep 17 00:00:00 2001 From: Johan Carlin Date: Sat, 9 May 2026 15:41:37 +0200 Subject: [PATCH 1/5] chore: bump upstream to v0.86.1 --- TODO_ARGS.md | 2 +- cmd/devcontainer/src/cli_metadata.json | 2 +- docs/upstream/command-matrix.json | 2 +- docs/upstream/command-reference.md | 2 +- docs/upstream/compatibility-baseline.json | 4 ++-- docs/upstream/compatibility-dashboard.md | 2 +- docs/upstream/compose-parity.md | 2 +- docs/upstream/parity-inventory.json | 2 +- docs/upstream/parity-inventory.md | 2 +- docs/upstream/test-coverage-map.json | 11 ++++++++++- docs/upstream/test-coverage-map.md | 7 ++++--- upstream | 2 +- 12 files changed, 25 insertions(+), 15 deletions(-) diff --git a/TODO_ARGS.md b/TODO_ARGS.md index e9274020f..739771f21 100644 --- a/TODO_ARGS.md +++ b/TODO_ARGS.md @@ -2,6 +2,6 @@ Unsupported CLI args for the current pinned upstream command surface. -- Upstream commit: `2d81ee3c9ed96a7312c18c7513a17933f8f66d41` +- Upstream commit: `6293ce5879399316f06287e42e710c0f8e5edfef` - Source: `upstream/src/spec-node/devContainersSpecCLI.ts` diff --git a/cmd/devcontainer/src/cli_metadata.json b/cmd/devcontainer/src/cli_metadata.json index 3cf08055b..58e347cac 100644 --- a/cmd/devcontainer/src/cli_metadata.json +++ b/cmd/devcontainer/src/cli_metadata.json @@ -1,5 +1,5 @@ { - "upstreamCommit": "2d81ee3c9ed96a7312c18c7513a17933f8f66d41", + "upstreamCommit": "6293ce5879399316f06287e42e710c0f8e5edfef", "sourcePath": "upstream/src/spec-node/devContainersSpecCLI.ts", "root": { "lines": [ diff --git a/docs/upstream/command-matrix.json b/docs/upstream/command-matrix.json index 3dee14450..25d6590e2 100644 --- a/docs/upstream/command-matrix.json +++ b/docs/upstream/command-matrix.json @@ -1,5 +1,5 @@ { - "upstreamCommit": "2d81ee3c9ed96a7312c18c7513a17933f8f66d41", + "upstreamCommit": "6293ce5879399316f06287e42e710c0f8e5edfef", "sourcePath": "upstream/src/spec-node/devContainersSpecCLI.ts", "topLevel": [ "up", diff --git a/docs/upstream/command-reference.md b/docs/upstream/command-reference.md index 7d9d47577..e4c9d32f6 100644 --- a/docs/upstream/command-reference.md +++ b/docs/upstream/command-reference.md @@ -2,7 +2,7 @@ Generated from the pinned upstream CLI command matrix. This is a compatibility baseline, not a native behavior reference. -- Upstream commit: `2d81ee3c9ed96a7312c18c7513a17933f8f66d41` +- Upstream commit: `6293ce5879399316f06287e42e710c0f8e5edfef` - Source: `upstream/src/spec-node/devContainersSpecCLI.ts` ## Top-Level Commands diff --git a/docs/upstream/compatibility-baseline.json b/docs/upstream/compatibility-baseline.json index a3eb562e4..68fa272d2 100644 --- a/docs/upstream/compatibility-baseline.json +++ b/docs/upstream/compatibility-baseline.json @@ -1,5 +1,5 @@ { "submodulePath": "upstream", - "pinnedCommit": "2d81ee3c9ed96a7312c18c7513a17933f8f66d41", - "compatibilityContract": "This repository targets upstream/ at commit 2d81ee3c9ed96a7312c18c7513a17933f8f66d41." + "pinnedCommit": "6293ce5879399316f06287e42e710c0f8e5edfef", + "compatibilityContract": "This repository targets upstream/ at commit 6293ce5879399316f06287e42e710c0f8e5edfef." } diff --git a/docs/upstream/compatibility-dashboard.md b/docs/upstream/compatibility-dashboard.md index 29480b802..cbfaf1f51 100644 --- a/docs/upstream/compatibility-dashboard.md +++ b/docs/upstream/compatibility-dashboard.md @@ -1,6 +1,6 @@ # Native Compatibility Dashboard -- Pinned upstream commit: `2d81ee3c9ed96a7312c18c7513a17933f8f66d41` +- Pinned upstream commit: `6293ce5879399316f06287e42e710c0f8e5edfef` - Pinned spec commit: `c95ffeed1d059abfe9ffbe79762dc2fa4e7c2421` - Command matrix source: `docs/upstream/command-matrix.json` - Native parity inventory: `docs/upstream/parity-inventory.md` diff --git a/docs/upstream/compose-parity.md b/docs/upstream/compose-parity.md index 59a44ec08..588e8efce 100644 --- a/docs/upstream/compose-parity.md +++ b/docs/upstream/compose-parity.md @@ -1,6 +1,6 @@ # Compose Parity Inventory -Pinned upstream CLI commit: `2d81ee3c9ed96a7312c18c7513a17933f8f66d41` +Pinned upstream CLI commit: `6293ce5879399316f06287e42e710c0f8e5edfef` This is a semantic parity note for the native Rust Compose path. It complements the generated command-matrix inventory in `docs/upstream/parity-inventory.md`, which only records static source references. diff --git a/docs/upstream/parity-inventory.json b/docs/upstream/parity-inventory.json index 017139f6a..b62a79dbc 100644 --- a/docs/upstream/parity-inventory.json +++ b/docs/upstream/parity-inventory.json @@ -1,5 +1,5 @@ { - "upstreamCommit": "2d81ee3c9ed96a7312c18c7513a17933f8f66d41", + "upstreamCommit": "6293ce5879399316f06287e42e710c0f8e5edfef", "sourcePath": "upstream/src/spec-node/devContainersSpecCLI.ts", "summary": { "commandPathsTotal": 20, diff --git a/docs/upstream/parity-inventory.md b/docs/upstream/parity-inventory.md index 8fdcd904d..d94773743 100644 --- a/docs/upstream/parity-inventory.md +++ b/docs/upstream/parity-inventory.md @@ -2,7 +2,7 @@ Generated from the pinned upstream CLI command matrix and static source evidence in the Rust implementation. -- Upstream commit: `2d81ee3c9ed96a7312c18c7513a17933f8f66d41` +- Upstream commit: `6293ce5879399316f06287e42e710c0f8e5edfef` - Source: `upstream/src/spec-node/devContainersSpecCLI.ts` - Declared upstream command paths present natively: `20/20` - Upstream options with a native source reference in mapped files: `200/200` diff --git a/docs/upstream/test-coverage-map.json b/docs/upstream/test-coverage-map.json index 6198fabdb..b3630c41d 100644 --- a/docs/upstream/test-coverage-map.json +++ b/docs/upstream/test-coverage-map.json @@ -1,5 +1,5 @@ { - "upstreamCommit": "2d81ee3c9ed96a7312c18c7513a17933f8f66d41", + "upstreamCommit": "6293ce5879399316f06287e42e710c0f8e5edfef", "suites": [ { "upstreamTest": "upstream/src/test/cli.build.test.ts", @@ -165,6 +165,15 @@ ], "notes": "Native read-configuration coverage validates generated local Feature sets, option values, published Feature customizations, metadata merge behavior, and runtime build smoke coverage validates install materialization." }, + { + "upstreamTest": "upstream/src/test/container-features/generateLockfile.test.ts", + "status": "covered", + "nativeTests": [ + "cmd/devcontainer/src/commands/configuration/tests/upgrade.rs", + "cmd/devcontainer/tests/runtime_build_smoke/features.rs" + ], + "notes": "Native lockfile generation coverage validates configured Feature filtering, additional-only Feature exclusion, trailing-newline writes, semantic frozen comparisons, corrupt lockfile failures, and build-path lockfile generation." + }, { "upstreamTest": "upstream/src/test/container-features/lifecycleHooks.test.ts", "status": "covered", diff --git a/docs/upstream/test-coverage-map.md b/docs/upstream/test-coverage-map.md index 9d5e93bfa..f503d188e 100644 --- a/docs/upstream/test-coverage-map.md +++ b/docs/upstream/test-coverage-map.md @@ -2,9 +2,9 @@ Machine-readable upstream test coverage inventory for the native Rust CLI. -- Upstream commit: `2d81ee3c9ed96a7312c18c7513a17933f8f66d41` -- Upstream tests inventoried: `35` -- Covered: `15` +- Upstream commit: `6293ce5879399316f06287e42e710c0f8e5edfef` +- Upstream tests inventoried: `36` +- Covered: `16` - Partial: `20` - Missing: `0` @@ -29,6 +29,7 @@ Machine-readable upstream test coverage inventory for the native Rust CLI. | `upstream/src/test/container-features/featureHelpers.test.ts` | partial | `cmd/devcontainer/src/commands/collections/feature_tests/materialize.rs`
`cmd/devcontainer/src/commands/collections/tests/feature_tests.rs` | Native helper coverage focuses on test materialization, not the full upstream helper surface. | | `upstream/src/test/container-features/featuresCLICommands.test.ts` | partial | `cmd/devcontainer/tests/cli_smoke/collections.rs`
`cmd/devcontainer/src/commands/collections/tests/features.rs`
`cmd/devcontainer/src/commands/collections/tests/feature_tests.rs`
`cmd/devcontainer/src/commands/collections/tests/publish.rs` | CLI coverage exists for Features commands, but published flows remain substitute-based. | | `upstream/src/test/container-features/generateFeaturesConfig.test.ts` | covered | `cmd/devcontainer/src/commands/configuration/tests/read.rs`
`cmd/devcontainer/tests/runtime_build_smoke/features.rs` | Native read-configuration coverage validates generated local Feature sets, option values, published Feature customizations, metadata merge behavior, and runtime build smoke coverage validates install materialization. | +| `upstream/src/test/container-features/generateLockfile.test.ts` | covered | `cmd/devcontainer/src/commands/configuration/tests/upgrade.rs`
`cmd/devcontainer/tests/runtime_build_smoke/features.rs` | Native lockfile generation coverage validates configured Feature filtering, additional-only Feature exclusion, trailing-newline writes, semantic frozen comparisons, corrupt lockfile failures, and build-path lockfile generation. | | `upstream/src/test/container-features/lifecycleHooks.test.ts` | covered | `cmd/devcontainer/tests/runtime_lifecycle_smoke.rs`
`cmd/devcontainer/tests/runtime_lifecycle_smoke/commands.rs`
`cmd/devcontainer/tests/runtime_lifecycle_smoke/selection.rs` | Native lifecycle smoke coverage now includes Feature-contributed hooks merged before devcontainer-level hooks, install-order-sensitive hook ordering, resume/run-user-commands paths, and secrets propagation through lifecycle exec. | | `upstream/src/test/container-features/lockfile.test.ts` | covered | `cmd/devcontainer/tests/cli_smoke/lockfile.rs`
`cmd/devcontainer/src/commands/configuration/tests/upgrade.rs` | Native lockfile coverage includes outdated, upgrade, dry-run, root-relative path handling, trailing-newline writes, missing frozen-lockfile errors, and workspace-local OCI layout mirrors for published Feature version and digest resolution. | | `upstream/src/test/container-features/registryCompatibilityOCI.test.ts` | partial | `cmd/devcontainer/src/commands/collections/tests/features.rs`
`cmd/devcontainer/tests/network_smoke/ghcr.rs` | Native coverage now includes OCI-manifest-shaped `features info manifest`, canonical ids, `publishedTags` output, and a dedicated anonymous GHCR manifest smoke check, but registry auth and broader live OCI pull flows are still missing. | diff --git a/upstream b/upstream index 2d81ee3c9..6293ce587 160000 --- a/upstream +++ b/upstream @@ -1 +1 @@ -Subproject commit 2d81ee3c9ed96a7312c18c7513a17933f8f66d41 +Subproject commit 6293ce5879399316f06287e42e710c0f8e5edfef From 1ac8bf52af90fec6f456a5eb4790687a893bf805 Mon Sep 17 00:00:00 2001 From: Johan Carlin Date: Sat, 9 May 2026 15:46:33 +0200 Subject: [PATCH 2/5] fix: align native lockfile generation with upstream --- .../commands/configuration/tests/upgrade.rs | 36 ++++++++++++++++- .../src/commands/configuration/upgrade.rs | 10 ++++- .../tests/runtime_build_smoke/features.rs | 40 +++++++++++++++++++ 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/cmd/devcontainer/src/commands/configuration/tests/upgrade.rs b/cmd/devcontainer/src/commands/configuration/tests/upgrade.rs index dcfe6b093..cdbe13149 100644 --- a/cmd/devcontainer/src/commands/configuration/tests/upgrade.rs +++ b/cmd/devcontainer/src/commands/configuration/tests/upgrade.rs @@ -212,7 +212,7 @@ fn ensure_native_lockfile_uses_shared_lockfile_format() { .expect("lockfile write"); let lockfile = fs::read_to_string(root.join(".devcontainer-lock.json")).expect("lockfile"); - assert!(!lockfile.ends_with('\n')); + assert!(lockfile.ends_with('\n')); let _ = fs::remove_dir_all(root); } @@ -230,11 +230,43 @@ fn upgrade_lockfile_uses_shared_lockfile_format() { .expect("lockfile payload"); let lockfile = fs::read_to_string(root.join(".devcontainer-lock.json")).expect("lockfile"); - assert!(!lockfile.ends_with('\n')); + assert!(lockfile.ends_with('\n')); let _ = fs::remove_dir_all(root); } +#[test] +fn ensure_native_lockfile_rejects_corrupt_existing_lockfile_when_generating() { + let root = unique_temp_dir(); + fs::create_dir_all(&root).expect("failed to create root"); + let config_file = root.join(".devcontainer.json"); + let lockfile_path = root.join(".devcontainer-lock.json"); + fs::write(&lockfile_path, "this is not json").expect("corrupt lockfile"); + + let error = ensure_native_lockfile( + &[ + "--workspace-folder".to_string(), + root.display().to_string(), + "--experimental-lockfile".to_string(), + ], + &config_file, + &json!({ + "image": "debian:bookworm", + "features": { + "ghcr.io/devcontainers/features/github-cli": {} + } + }), + ) + .expect_err("corrupt lockfile error"); + + assert!(error.contains("line 1 column"), "{error}"); + assert_eq!( + fs::read_to_string(lockfile_path).expect("lockfile"), + "this is not json" + ); + let _ = fs::remove_dir_all(root); +} + #[test] fn ensure_native_lockfile_reports_missing_frozen_lockfile() { let root = unique_temp_dir(); diff --git a/cmd/devcontainer/src/commands/configuration/upgrade.rs b/cmd/devcontainer/src/commands/configuration/upgrade.rs index 9856d4f28..8a7824ade 100644 --- a/cmd/devcontainer/src/commands/configuration/upgrade.rs +++ b/cmd/devcontainer/src/commands/configuration/upgrade.rs @@ -84,8 +84,12 @@ pub(super) fn ensure_native_lockfile( .or_else(|| config_file.parent().map(Path::to_path_buf)); let generated = generate_lockfile(configuration, workspace_folder.as_deref())?; let path = lockfile_path(config_file); + let existing = if path.exists() || common::has_flag(args, "--experimental-frozen-lockfile") { + read_lockfile(path.clone())? + } else { + None + }; if common::has_flag(args, "--experimental-frozen-lockfile") { - let existing = read_lockfile(path.clone())?; let Some(existing) = existing else { return Err("Lockfile does not exist.".to_string()); }; @@ -104,7 +108,9 @@ pub(super) fn ensure_native_lockfile( } fn serialized_lockfile(lockfile: &Lockfile) -> Result { - serde_json::to_string_pretty(lockfile).map_err(|error| error.to_string()) + serde_json::to_string_pretty(lockfile) + .map(|json| format!("{json}\n")) + .map_err(|error| error.to_string()) } #[cfg(test)] diff --git a/cmd/devcontainer/tests/runtime_build_smoke/features.rs b/cmd/devcontainer/tests/runtime_build_smoke/features.rs index 2f114343a..91cb03f85 100644 --- a/cmd/devcontainer/tests/runtime_build_smoke/features.rs +++ b/cmd/devcontainer/tests/runtime_build_smoke/features.rs @@ -381,6 +381,46 @@ fn build_writes_feature_lockfile_when_requested() { assert!(lockfile.contains("\"resolved\":")); } +#[test] +fn build_omits_additional_only_features_from_generated_lockfile() { + let harness = RuntimeHarness::new(); + let workspace = harness.workspace(); + fs::create_dir_all(workspace.join(".devcontainer")).expect("workspace config dir"); + write_devcontainer_config( + &workspace, + "{\n \"image\": \"debian:bookworm\",\n \"features\": {\n \"ghcr.io/devcontainers/features/git:1.0\": {}\n }\n}\n", + ); + + let fake_podman = harness.fake_podman.to_string_lossy().to_string(); + let output = harness.run( + &[ + "build", + "--docker-path", + fake_podman.as_str(), + "--workspace-folder", + workspace.to_string_lossy().as_ref(), + "--additional-features", + "{\"ghcr.io/devcontainers/features/github-cli\":{}}", + "--experimental-lockfile", + ], + &[], + ); + + assert!(output.status.success(), "{output:?}"); + let lockfile: serde_json::Value = serde_json::from_str( + &fs::read_to_string( + workspace + .join(".devcontainer") + .join("devcontainer-lock.json"), + ) + .expect("lockfile"), + ) + .expect("lockfile json"); + let features = lockfile["features"].as_object().expect("features object"); + assert!(features.contains_key("ghcr.io/devcontainers/features/git:1.0")); + assert!(!features.contains_key("ghcr.io/devcontainers/features/github-cli")); +} + #[test] fn build_rejects_outdated_frozen_feature_lockfile() { let harness = RuntimeHarness::new(); From c5f8313c0a7c3fcbe341ed9e2b0e91b8d0f8b5a0 Mon Sep 17 00:00:00 2001 From: Johan Carlin Date: Sat, 9 May 2026 15:57:06 +0200 Subject: [PATCH 3/5] chore: remove TODO args report --- Makefile | 6 +-- TODO_ARGS.md | 7 --- build/generate-todo-args.js | 95 ------------------------------------- package.json | 4 +- 4 files changed, 2 insertions(+), 110 deletions(-) delete mode 100644 TODO_ARGS.md delete mode 100644 build/generate-todo-args.js diff --git a/Makefile b/Makefile index 50230f642..2aaa9cfad 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,6 @@ npm-publish-workflow-check \ check-parity-inventory \ check-cli-metadata \ - check-todo-args \ check-compatibility-dashboard \ check-upstream-test-coverage \ upstream-compatibility @@ -30,7 +29,7 @@ RUST_MANIFEST := cmd/devcontainer/Cargo.toml RELEASE_BINARY := ./cmd/devcontainer/target/release/devcontainer -tests: rust-fmt rust-clippy rust-check rust-tests build-release standalone-artifact-smoke pypi-wheel-smoke native-only-startup-contract acceptance-fixtures-check command-matrix-drift-check schema-drift-check parity-harness no-node-runtime npm-wrapper-check npm-publish-script-check npm-package-smoke homebrew-distribution-check npm-publish-workflow-check check-parity-inventory check-cli-metadata check-todo-args check-compatibility-dashboard check-upstream-test-coverage upstream-compatibility +tests: rust-fmt rust-clippy rust-check rust-tests build-release standalone-artifact-smoke pypi-wheel-smoke native-only-startup-contract acceptance-fixtures-check command-matrix-drift-check schema-drift-check parity-harness no-node-runtime npm-wrapper-check npm-publish-script-check npm-package-smoke homebrew-distribution-check npm-publish-workflow-check check-parity-inventory check-cli-metadata check-compatibility-dashboard check-upstream-test-coverage upstream-compatibility rust-fmt: cargo fmt --manifest-path $(RUST_MANIFEST) --all -- --check @@ -101,9 +100,6 @@ check-parity-inventory: check-cli-metadata: node build/generate-cli-metadata.js --check -check-todo-args: - node build/generate-todo-args.js --check - check-compatibility-dashboard: node build/generate-compatibility-dashboard.js --check diff --git a/TODO_ARGS.md b/TODO_ARGS.md deleted file mode 100644 index 739771f21..000000000 --- a/TODO_ARGS.md +++ /dev/null @@ -1,7 +0,0 @@ -# TODO_ARGS - -Unsupported CLI args for the current pinned upstream command surface. - -- Upstream commit: `6293ce5879399316f06287e42e710c0f8e5edfef` -- Source: `upstream/src/spec-node/devContainersSpecCLI.ts` - diff --git a/build/generate-todo-args.js b/build/generate-todo-args.js deleted file mode 100644 index 412072f45..000000000 --- a/build/generate-todo-args.js +++ /dev/null @@ -1,95 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) devcontainer-rs contributors. - * Licensed under the MIT License. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const fs = require('fs'); -const path = require('path'); - -const { generateCliMetadata } = require('./generate-cli-metadata'); - -const repositoryRoot = path.join(__dirname, '..'); -const outputPath = path.join(repositoryRoot, 'TODO_ARGS.md'); - -function optionDisplay(option) { - const aliases = option.aliases.length - ? ` (aliases: ${option.aliases.map(alias => `\`-${alias}\``).join(', ')})` - : ''; - const visibility = option.visible ? '' : ' [hidden upstream option]'; - const description = option.description || 'No upstream help description available.'; - return `- \`--${option.name}\`${aliases}${visibility}: ${description}`; -} - -function renderTodoArgs(metadata) { - const commands = metadata.commands - .filter( - command => - command.unsupportedOptions.length || command.unsupportedPositionals.length, - ) - .sort((left, right) => left.path.localeCompare(right.path)); - - return [ - '# TODO_ARGS', - '', - 'Unsupported CLI args for the current pinned upstream command surface.', - '', - `- Upstream commit: \`${metadata.upstreamCommit}\``, - `- Source: \`${metadata.sourcePath}\``, - '', - ...commands.flatMap(command => { - const optionsByName = new Map(command.options.map(option => [option.name, option])); - const optionEntries = command.unsupportedOptions.map(name => { - const option = optionsByName.get(name) || { - name, - aliases: [], - description: null, - visible: false, - }; - return optionDisplay(option); - }); - const positionalEntries = command.unsupportedPositionals.map( - name => `- positional \`${name}\``, - ); - return [ - `## \`${command.path}\``, - '', - ...optionEntries, - ...positionalEntries, - '', - ]; - }), - ].join('\n'); -} - -function writeTodoArgs(text) { - fs.writeFileSync(outputPath, `${text}\n`); -} - -function compareToCommitted(text) { - if (!fs.existsSync(outputPath)) { - throw new Error(`Missing committed TODO args file: ${path.relative(repositoryRoot, outputPath)}`); - } - return fs.readFileSync(outputPath, 'utf8') === `${text}\n`; -} - -if (require.main === module) { - const metadata = generateCliMetadata(); - const text = renderTodoArgs(metadata); - if (process.argv.includes('--check')) { - if (!compareToCommitted(text)) { - console.error('Committed TODO_ARGS.md is out of date. Run node build/generate-todo-args.js'); - process.exit(1); - } - console.log('[todo-args] committed TODO_ARGS.md matches current metadata.'); - } else { - writeTodoArgs(text); - console.log(`[todo-args] wrote ${path.relative(repositoryRoot, outputPath)}`); - } -} - -module.exports = { - renderTodoArgs, - writeTodoArgs, -}; diff --git a/package.json b/package.json index 28f3f1a45..4b9bd33ae 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,12 @@ }, "scripts": { "test": "npm run check", - "check": "node build/check-upstream-submodule.js && node build/check-upstream-compatibility.js && node build/generate-command-matrix.js --check && node build/generate-cli-reference.js --check && node build/generate-parity-inventory.js --check && node build/generate-cli-metadata.js --check && node build/generate-todo-args.js --check && node build/generate-compatibility-dashboard.js --check && node build/check-upstream-test-coverage.js && node build/check-spec-drift.js && node build/check-parity-harness.js && node build/check-native-only.js && node build/check-no-node-runtime.js && node --test build/test-npm-wrapper.js && node --test build/test-publish-npm-packages.js && node --test build/test-npm-package-smoke.js && node build/check-npm-packages.js && node build/check-homebrew-distribution.js && node build/check-npm-publish-workflow.js && node build/check-repo-branding.js && node build/check-devcontainer-config.js", + "check": "node build/check-upstream-submodule.js && node build/check-upstream-compatibility.js && node build/generate-command-matrix.js --check && node build/generate-cli-reference.js --check && node build/generate-parity-inventory.js --check && node build/generate-cli-metadata.js --check && node build/generate-compatibility-dashboard.js --check && node build/check-upstream-test-coverage.js && node build/check-spec-drift.js && node build/check-parity-harness.js && node build/check-native-only.js && node build/check-no-node-runtime.js && node --test build/test-npm-wrapper.js && node --test build/test-publish-npm-packages.js && node --test build/test-npm-package-smoke.js && node build/check-npm-packages.js && node build/check-homebrew-distribution.js && node build/check-npm-publish-workflow.js && node build/check-repo-branding.js && node build/check-devcontainer-config.js", "install-git-hooks": "./scripts/install-git-hooks.sh", "generate-command-matrix": "node build/generate-command-matrix.js", "generate-cli-reference": "node build/generate-cli-reference.js", "generate-parity-inventory": "node build/generate-parity-inventory.js", "generate-cli-metadata": "node build/generate-cli-metadata.js", - "generate-todo-args": "node build/generate-todo-args.js", "generate-compatibility-dashboard": "node build/generate-compatibility-dashboard.js", "generate-upstream-test-coverage": "node build/generate-upstream-test-coverage.js", "prepare-npm-packages": "node build/prepare-npm-packages.js", @@ -30,7 +29,6 @@ "check-cli-reference": "node build/generate-cli-reference.js --check", "check-parity-inventory": "node build/generate-parity-inventory.js --check", "check-cli-metadata": "node build/generate-cli-metadata.js --check", - "check-todo-args": "node build/generate-todo-args.js --check", "check-compatibility-dashboard": "node build/generate-compatibility-dashboard.js --check", "check-upstream-test-coverage": "node build/check-upstream-test-coverage.js", "check-devcontainer-config": "node build/check-devcontainer-config.js", From 0773d354e03ce1730a123846bdedf1b5c38b42ec Mon Sep 17 00:00:00 2001 From: Johan Carlin Date: Sat, 9 May 2026 16:01:28 +0200 Subject: [PATCH 4/5] chore: remove repo branding check --- build/check-repo-branding.js | 85 ------------------------------------ docs/architecture.md | 4 +- package.json | 3 +- 3 files changed, 3 insertions(+), 89 deletions(-) delete mode 100644 build/check-repo-branding.js diff --git a/build/check-repo-branding.js b/build/check-repo-branding.js deleted file mode 100644 index 5fc58c95e..000000000 --- a/build/check-repo-branding.js +++ /dev/null @@ -1,85 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) devcontainer-rs contributors. - * Licensed under the MIT License. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const cp = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -const repositoryRoot = path.join(__dirname, '..'); -const ignoredFiles = new Set([ - 'build/check-repo-branding.js', -]); -const ignoredPrefixes = [ - 'spec', - 'spec/', - 'upstream', - 'upstream/', -]; - -const forbiddenPatterns = [ - /\bMicrosoft\b/i, - /\bmcr\.microsoft\.com\b/i, - /\b(?:go\.)?microsoft\.com\b/i, - /\baka\.ms\b/i, -]; - -function run(command, args) { - const result = cp.spawnSync(command, args, { - cwd: repositoryRoot, - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - }); - - if (result.error || result.status !== 0) { - const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim(); - throw new Error(output || `${command} ${args.join(' ')} failed`); - } - - return result.stdout; -} - -function shouldScan(relativePath) { - if (ignoredFiles.has(relativePath)) { - return false; - } - - return !ignoredPrefixes.some(prefix => relativePath.startsWith(prefix)); -} - -function main() { - const trackedFiles = run('git', ['ls-files', '-z']) - .split('\0') - .filter(Boolean) - .filter(shouldScan); - - const failures = []; - - for (const relativePath of trackedFiles) { - const absolutePath = path.join(repositoryRoot, relativePath); - if (!fs.statSync(absolutePath).isFile()) { - continue; - } - const content = fs.readFileSync(absolutePath, 'utf8'); - for (const pattern of forbiddenPatterns) { - if (pattern.test(content)) { - failures.push(`${relativePath}: ${pattern}`); - } - } - } - - if (failures.length > 0) { - console.error('[repo-branding] repository-owned files still contain forbidden branding or Microsoft-hosted URLs:'); - for (const failure of failures) { - console.error(` - ${failure}`); - } - process.exit(1); - } - - console.log('[repo-branding] repository-owned files contain no Microsoft branding.'); -} - -main(); diff --git a/docs/architecture.md b/docs/architecture.md index 345e7bc16..f4f4fad2a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -48,13 +48,13 @@ Dependency direction: `commands/*` may depend on `runtime/*`, but `runtime/*` sh - The runtime smoke suite is split by concern: build, container lifecycle, context resolution, exec behavior, and lifecycle behavior. - `acceptance/` holds repo-owned manual acceptance fixtures and the suite manifest for contributor-guided runtime checks. - Repo-owned compatibility fixtures live under `src/test/parity/`. -- Node guard scripts in `build/` cover upstream/spec drift, command-matrix drift, native-only startup, no-node-runtime regressions, and the parity harness. +- Node scripts in `build/` are repository maintenance tooling. They cover upstream/spec drift, generated CLI metadata/reference files, parity inventory/dashboard files, upstream test coverage, acceptance fixture manifests, native-only startup, no-node-runtime regressions, the parity harness, distribution package checks, and repo-owned devcontainer config validation. ## Compatibility assets - `upstream/` is the only canonical location for upstream CLI TypeScript sources. - `spec/` is the only canonical location for upstream schemas and normative spec docs. -- Root-level Node scripts are compatibility tooling only; they must not become part of runtime execution or release packaging. +- Node maintenance scripts must not become part of runtime execution. Release packaging should consume built artifacts through the dedicated package-preparation scripts rather than adding a Node bridge to the native CLI. ## Maintenance rules diff --git a/package.json b/package.json index 4b9bd33ae..3c11868e3 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "scripts": { "test": "npm run check", - "check": "node build/check-upstream-submodule.js && node build/check-upstream-compatibility.js && node build/generate-command-matrix.js --check && node build/generate-cli-reference.js --check && node build/generate-parity-inventory.js --check && node build/generate-cli-metadata.js --check && node build/generate-compatibility-dashboard.js --check && node build/check-upstream-test-coverage.js && node build/check-spec-drift.js && node build/check-parity-harness.js && node build/check-native-only.js && node build/check-no-node-runtime.js && node --test build/test-npm-wrapper.js && node --test build/test-publish-npm-packages.js && node --test build/test-npm-package-smoke.js && node build/check-npm-packages.js && node build/check-homebrew-distribution.js && node build/check-npm-publish-workflow.js && node build/check-repo-branding.js && node build/check-devcontainer-config.js", + "check": "node build/check-upstream-submodule.js && node build/check-upstream-compatibility.js && node build/generate-command-matrix.js --check && node build/generate-cli-reference.js --check && node build/generate-parity-inventory.js --check && node build/generate-cli-metadata.js --check && node build/generate-compatibility-dashboard.js --check && node build/check-upstream-test-coverage.js && node build/check-spec-drift.js && node build/check-parity-harness.js && node build/check-native-only.js && node build/check-no-node-runtime.js && node --test build/test-npm-wrapper.js && node --test build/test-publish-npm-packages.js && node --test build/test-npm-package-smoke.js && node build/check-npm-packages.js && node build/check-homebrew-distribution.js && node build/check-npm-publish-workflow.js && node build/check-devcontainer-config.js", "install-git-hooks": "./scripts/install-git-hooks.sh", "generate-command-matrix": "node build/generate-command-matrix.js", "generate-cli-reference": "node build/generate-cli-reference.js", @@ -32,7 +32,6 @@ "check-compatibility-dashboard": "node build/generate-compatibility-dashboard.js --check", "check-upstream-test-coverage": "node build/check-upstream-test-coverage.js", "check-devcontainer-config": "node build/check-devcontainer-config.js", - "check-repo-branding": "node build/check-repo-branding.js", "check-parity-harness": "node build/check-parity-harness.js", "check-no-node-runtime": "node build/check-no-node-runtime.js", "check-npm-wrapper": "node --test build/test-npm-wrapper.js", From e5fc48a81ae725683f8246b76c8d3afe03e0401d Mon Sep 17 00:00:00 2001 From: Johan Carlin Date: Sat, 9 May 2026 16:36:57 +0200 Subject: [PATCH 5/5] Validate feature lockfiles before runtime builds --- .../src/commands/configuration/mod.rs | 8 +++ .../src/commands/configuration/upgrade.rs | 52 ++++++++++++++++--- cmd/devcontainer/src/runtime/build.rs | 10 ++++ cmd/devcontainer/src/runtime/compose/mod.rs | 7 +++ .../tests/runtime_build_smoke/compose.rs | 43 +++++++++++++++ .../tests/runtime_build_smoke/features.rs | 41 +++++++++++++++ 6 files changed, 153 insertions(+), 8 deletions(-) diff --git a/cmd/devcontainer/src/commands/configuration/mod.rs b/cmd/devcontainer/src/commands/configuration/mod.rs index 273907499..2ac451040 100644 --- a/cmd/devcontainer/src/commands/configuration/mod.rs +++ b/cmd/devcontainer/src/commands/configuration/mod.rs @@ -110,6 +110,14 @@ pub(crate) fn ensure_native_lockfile( upgrade::ensure_native_lockfile(args, config_file, configuration) } +pub(crate) fn validate_native_lockfile( + args: &[String], + config_file: &std::path::Path, + configuration: &Value, +) -> Result<(), String> { + upgrade::validate_native_lockfile(args, config_file, configuration) +} + pub(crate) fn should_use_native_read_configuration(args: &[String]) -> bool { read::should_use_native_read_configuration(args) } diff --git a/cmd/devcontainer/src/commands/configuration/upgrade.rs b/cmd/devcontainer/src/commands/configuration/upgrade.rs index 8a7824ade..c01746941 100644 --- a/cmd/devcontainer/src/commands/configuration/upgrade.rs +++ b/cmd/devcontainer/src/commands/configuration/upgrade.rs @@ -73,9 +73,7 @@ pub(super) fn ensure_native_lockfile( config_file: &Path, configuration: &Value, ) -> Result<(), String> { - let wants_lockfile = common::has_flag(args, "--experimental-lockfile") - || common::has_flag(args, "--experimental-frozen-lockfile"); - if !wants_lockfile { + if !wants_native_lockfile(args) { return Ok(()); } @@ -84,11 +82,7 @@ pub(super) fn ensure_native_lockfile( .or_else(|| config_file.parent().map(Path::to_path_buf)); let generated = generate_lockfile(configuration, workspace_folder.as_deref())?; let path = lockfile_path(config_file); - let existing = if path.exists() || common::has_flag(args, "--experimental-frozen-lockfile") { - read_lockfile(path.clone())? - } else { - None - }; + let existing = existing_native_lockfile(args, &path)?; if common::has_flag(args, "--experimental-frozen-lockfile") { let Some(existing) = existing else { return Err("Lockfile does not exist.".to_string()); @@ -107,6 +101,48 @@ pub(super) fn ensure_native_lockfile( Ok(()) } +pub(super) fn validate_native_lockfile( + args: &[String], + config_file: &Path, + configuration: &Value, +) -> Result<(), String> { + if !wants_native_lockfile(args) { + return Ok(()); + } + + let path = lockfile_path(config_file); + let existing = existing_native_lockfile(args, &path)?; + if common::has_flag(args, "--experimental-frozen-lockfile") { + let Some(existing) = existing else { + return Err("Lockfile does not exist.".to_string()); + }; + let workspace_folder = common::parse_option_value(args, "--workspace-folder") + .map(PathBuf::from) + .or_else(|| config_file.parent().map(Path::to_path_buf)); + let generated = generate_lockfile(configuration, workspace_folder.as_deref())?; + if existing != generated { + return Err(format!( + "Lockfile at {} is out of date for the current feature configuration", + path.display() + )); + } + } + Ok(()) +} + +fn wants_native_lockfile(args: &[String]) -> bool { + common::has_flag(args, "--experimental-lockfile") + || common::has_flag(args, "--experimental-frozen-lockfile") +} + +fn existing_native_lockfile(args: &[String], path: &Path) -> Result, String> { + if path.exists() || common::has_flag(args, "--experimental-frozen-lockfile") { + read_lockfile(path.to_path_buf()) + } else { + Ok(None) + } +} + fn serialized_lockfile(lockfile: &Lockfile) -> Result { serde_json::to_string_pretty(lockfile) .map(|json| format!("{json}\n")) diff --git a/cmd/devcontainer/src/runtime/build.rs b/cmd/devcontainer/src/runtime/build.rs index e99d6fc5e..411f12f2a 100644 --- a/cmd/devcontainer/src/runtime/build.rs +++ b/cmd/devcontainer/src/runtime/build.rs @@ -60,6 +60,11 @@ pub(crate) fn build_image(resolved: &ResolvedConfig, args: &[String]) -> Result< .to_string() })?; return if let Some(feature_support) = feature_support { + configuration::validate_native_lockfile( + args, + &resolved.config_file, + &resolved.configuration, + )?; let image_name = common::parse_option_value(args, "--image-name") .unwrap_or_else(|| default_image_name(&resolved.workspace_folder)); let built = @@ -79,6 +84,11 @@ pub(crate) fn build_image(resolved: &ResolvedConfig, args: &[String]) -> Result< let image_name = common::parse_option_value(args, "--image-name") .unwrap_or_else(|| default_image_name(&resolved.workspace_folder)); if let Some(feature_support) = feature_support { + configuration::validate_native_lockfile( + args, + &resolved.config_file, + &resolved.configuration, + )?; let base_image = format!("{image_name}-base"); build_base_image(resolved, args, &base_image)?; let built = build_feature_image( diff --git a/cmd/devcontainer/src/runtime/compose/mod.rs b/cmd/devcontainer/src/runtime/compose/mod.rs index f1e1b40ff..0d24826a5 100644 --- a/cmd/devcontainer/src/runtime/compose/mod.rs +++ b/cmd/devcontainer/src/runtime/compose/mod.rs @@ -79,6 +79,13 @@ pub(crate) fn build_service(resolved: &ResolvedConfig, args: &[String]) -> Resul &resolved.config_file, &resolved.configuration, )?; + if feature_support.is_some() { + configuration::validate_native_lockfile( + args, + &resolved.config_file, + &resolved.configuration, + )?; + } if spec.has_build { let build_override_file = override_file::compose_build_override_file(&spec, args)?; diff --git a/cmd/devcontainer/tests/runtime_build_smoke/compose.rs b/cmd/devcontainer/tests/runtime_build_smoke/compose.rs index 0ef45a505..f1b193180 100644 --- a/cmd/devcontainer/tests/runtime_build_smoke/compose.rs +++ b/cmd/devcontainer/tests/runtime_build_smoke/compose.rs @@ -240,6 +240,49 @@ fn compose_build_layers_features_on_top_of_service_images() { assert!(invocations.contains("build --tag example/native-compose:featured")); } +#[test] +fn compose_build_rejects_corrupt_existing_feature_lockfile_before_build() { + let harness = RuntimeHarness::new(); + let workspace = harness.workspace(); + let config_dir = workspace.join(".devcontainer"); + fs::create_dir_all(&config_dir).expect("workspace config dir"); + fs::write( + config_dir.join("docker-compose.yml"), + "services:\n app:\n image: example/native-compose:featured\n", + ) + .expect("compose"); + write_devcontainer_config( + &workspace, + "{\n \"dockerComposeFile\": \"docker-compose.yml\",\n \"service\": \"app\",\n \"workspaceFolder\": \"/workspace\",\n \"features\": {\n \"ghcr.io/devcontainers/features/git:1.0\": {}\n }\n}\n", + ); + fs::write( + config_dir.join("devcontainer-lock.json"), + "this is not json", + ) + .expect("corrupt lockfile"); + + let fake_podman = harness.fake_podman.to_string_lossy().to_string(); + let output = harness.run( + &[ + "build", + "--docker-path", + fake_podman.as_str(), + "--workspace-folder", + workspace.to_string_lossy().as_ref(), + "--experimental-lockfile", + ], + &[], + ); + + assert!(!output.status.success(), "{output:?}"); + let stderr = String::from_utf8(output.stderr).expect("utf8 stderr"); + assert!(stderr.contains("line 1 column"), "{stderr}"); + let invocations = + fs::read_to_string(harness.log_dir.join("invocations.log")).unwrap_or_default(); + assert!(!invocations.contains("build "), "{invocations}"); + assert!(!invocations.contains("push "), "{invocations}"); +} + #[test] fn build_rejects_cache_to_for_compose_builds() { let harness = RuntimeHarness::new(); diff --git a/cmd/devcontainer/tests/runtime_build_smoke/features.rs b/cmd/devcontainer/tests/runtime_build_smoke/features.rs index 91cb03f85..eb746f8d9 100644 --- a/cmd/devcontainer/tests/runtime_build_smoke/features.rs +++ b/cmd/devcontainer/tests/runtime_build_smoke/features.rs @@ -381,6 +381,47 @@ fn build_writes_feature_lockfile_when_requested() { assert!(lockfile.contains("\"resolved\":")); } +#[test] +fn build_rejects_corrupt_existing_feature_lockfile_before_build_or_push() { + let harness = RuntimeHarness::new(); + let workspace = harness.workspace(); + let config_dir = workspace.join(".devcontainer"); + fs::create_dir_all(&config_dir).expect("workspace config dir"); + write_devcontainer_config( + &workspace, + "{\n \"image\": \"debian:bookworm\",\n \"features\": {\n \"ghcr.io/devcontainers/features/git:1.0\": {}\n }\n}\n", + ); + fs::write( + config_dir.join("devcontainer-lock.json"), + "this is not json", + ) + .expect("corrupt lockfile"); + + let fake_podman = harness.fake_podman.to_string_lossy().to_string(); + let output = harness.run( + &[ + "build", + "--docker-path", + fake_podman.as_str(), + "--workspace-folder", + workspace.to_string_lossy().as_ref(), + "--image-name", + "example/native-build:corrupt-lockfile", + "--push", + "--experimental-lockfile", + ], + &[], + ); + + assert!(!output.status.success(), "{output:?}"); + let stderr = String::from_utf8(output.stderr).expect("utf8 stderr"); + assert!(stderr.contains("line 1 column"), "{stderr}"); + let invocations = + fs::read_to_string(harness.log_dir.join("invocations.log")).unwrap_or_default(); + assert!(!invocations.contains("build "), "{invocations}"); + assert!(!invocations.contains("push "), "{invocations}"); +} + #[test] fn build_omits_additional_only_features_from_generated_lockfile() { let harness = RuntimeHarness::new();