Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,9 @@ When asked to update upstream:
- Aggregated schema view: `spec/schemas/devContainer.schema.json`.
- Feature schema: `spec/schemas/devContainerFeature.schema.json`.
- Normative config behavior reference: `spec/docs/specs/devcontainerjson-reference.md`.

## Reviewing
- Reviews are by the current feature branch vs main unless told otherwise
- A common problem is that features are not implemented fully. Always call out if the code is skipping functionality, stubbing things out, or only implementing particular paths
- Tests should verify actual behaviour. Call out superficial tests that use too many mocks, or boil down to just asserting that functions are called, or that a variable has a particular name in a source file, etc
- Always check that new functionality tracks upstream/ and spec/ - the solution in our code should be a superset of upstream. Extensions are fine, but missing parity should be brought up
54 changes: 53 additions & 1 deletion cmd/devcontainer/src/cli_metadata.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"upstreamCommit": "6293ce5879399316f06287e42e710c0f8e5edfef",
"upstreamCommit": "65f98a518a1f62355a08e6f38e9d6bfb9a0d8ac9",
"sourcePath": "upstream/src/spec-node/devContainersSpecCLI.ts",
"root": {
"lines": [
Expand Down Expand Up @@ -428,6 +428,20 @@
],
"positionalNames": []
},
{
"text": " --no-lockfile Disable lockfile generation and verification. [boolean] [default: false]",
"optionNames": [
"no-lockfile"
],
"positionalNames": []
},
{
"text": " --frozen-lockfile Ensure lockfile exists and remains unchanged; fail otherwise. [boolean] [default: false]",
"optionNames": [
"frozen-lockfile"
],
"positionalNames": []
},
{
"text": " --include-configuration Include configuration in result. [boolean] [default: false]",
"optionNames": [
Expand Down Expand Up @@ -552,6 +566,12 @@
"description": null,
"visible": false
},
{
"name": "frozen-lockfile",
"aliases": [],
"description": "Ensure lockfile exists and remains unchanged; fail otherwise. [boolean] [default: false]",
"visible": true
},
{
"name": "gpu-availability",
"aliases": [],
Expand Down Expand Up @@ -606,6 +626,12 @@
"description": "Mount the workspace using its Git root. [boolean] [default: true]",
"visible": true
},
{
"name": "no-lockfile",
"aliases": [],
"description": "Disable lockfile generation and verification. [boolean] [default: false]",
"visible": true
},
{
"name": "omit-config-remote-env-from-metadata",
"aliases": [],
Expand Down Expand Up @@ -1218,6 +1244,20 @@
"additional-features"
],
"positionalNames": []
},
{
"text": " --no-lockfile Disable lockfile generation and verification. [boolean] [default: false]",
"optionNames": [
"no-lockfile"
],
"positionalNames": []
},
{
"text": " --frozen-lockfile Ensure lockfile exists and remains unchanged; fail otherwise. [boolean] [default: false]",
"optionNames": [
"frozen-lockfile"
],
"positionalNames": []
}
],
"options": [
Expand Down Expand Up @@ -1275,6 +1315,12 @@
"description": null,
"visible": false
},
{
"name": "frozen-lockfile",
"aliases": [],
"description": "Ensure lockfile exists and remains unchanged; fail otherwise. [boolean] [default: false]",
"visible": true
},
{
"name": "image-name",
"aliases": [],
Expand Down Expand Up @@ -1305,6 +1351,12 @@
"description": "Builds the image with `--no-cache`. [boolean] [default: false]",
"visible": true
},
{
"name": "no-lockfile",
"aliases": [],
"description": "Disable lockfile generation and verification. [boolean] [default: false]",
"visible": true
},
{
"name": "omit-syntax-directive",
"aliases": [],
Expand Down
2 changes: 1 addition & 1 deletion cmd/devcontainer/src/commands/collections/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub(super) fn build_features_resolve_dependencies_payload(
args: &[String],
) -> Result<Value, String> {
let (workspace_folder, config_file, configuration) = common::load_resolved_config(args)?;
let ordered = crate::commands::configuration::resolve_feature_support(
let ordered = crate::commands::configuration::resolve_feature_support_without_lockfile(
args,
&workspace_folder,
&config_file,
Expand Down
11 changes: 11 additions & 0 deletions cmd/devcontainer/src/commands/collections/oci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,17 @@ pub(crate) fn resolve_feature_artifact(
resolve_feature_artifact_for_reference(&parsed, workspace_folder, &CurlTransport)
}

pub(crate) fn resolve_feature_artifact_with_digest(
reference: &str,
manifest_digest: &str,
workspace_folder: Option<&Path>,
) -> Result<OciFeatureArtifact, String> {
let mut parsed = parse_oci_reference(reference)
.ok_or_else(|| format!("Invalid OCI Feature reference: {reference}"))?;
parsed.digest = Some(manifest_digest.to_string());
resolve_feature_artifact_for_reference(&parsed, workspace_folder, &CurlTransport)
}

pub(crate) fn list_feature_tags(
reference: &str,
workspace_folder: Option<&Path>,
Expand Down
37 changes: 37 additions & 0 deletions cmd/devcontainer/src/commands/collections/tests/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,43 @@ fn feature_dependency_resolution_respects_override_order() {
let _ = fs::remove_dir_all(root);
}

#[test]
fn feature_dependency_resolution_ignores_existing_lockfile() {
let root = unique_temp_dir();
let config_dir = root.join(".devcontainer");
let feature_dir = config_dir.join("local-feature");
fs::create_dir_all(&feature_dir).expect("failed to create feature directory");
fs::write(
feature_dir.join("devcontainer-feature.json"),
"{\n \"id\": \"local-feature\",\n \"name\": \"Local Feature\",\n \"version\": \"1.0.0\"\n}\n",
)
.expect("failed to write feature manifest");
fs::write(feature_dir.join("install.sh"), "#!/bin/sh\nset -eu\n")
.expect("failed to write feature install script");
fs::write(
config_dir.join("devcontainer.json"),
"{\n \"image\": \"debian:bookworm\",\n \"features\": {\n \"./local-feature\": {}\n }\n}\n",
)
.expect("failed to write config");
fs::write(
config_dir.join("devcontainer-lock.json"),
"this is not json",
)
.expect("failed to write corrupt lockfile");

let payload = build_features_resolve_dependencies_payload(&[
"--workspace-folder".to_string(),
root.display().to_string(),
])
.expect("payload should not read lockfile");

let features = payload["resolvedFeatures"]
.as_array()
.expect("resolved features");
assert_eq!(features, &[serde_json::json!("./local-feature")]);
let _ = fs::remove_dir_all(root);
}

#[test]
fn feature_dependency_resolution_matches_upstream_local_option_round_order() {
let root = copy_upstream_fixture("feature-dependencies/dependsOn/local-with-options");
Expand Down
22 changes: 12 additions & 10 deletions cmd/devcontainer/src/commands/configuration/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,16 +178,6 @@ pub(super) fn latest_version(base: &str, workspace_folder: Option<&Path>) -> Opt
.map(|entry| entry.version)
}

pub(super) fn catalog_entry_for_version(
base: &str,
version: &str,
workspace_folder: Option<&Path>,
) -> Option<CatalogEntry> {
catalog_entries(base, workspace_folder)?
.into_iter()
.find(|entry| entry.version == version)
}

fn local_oci_layout_entries(base: &str, workspace_folder: Option<&Path>) -> Vec<CatalogEntry> {
let Some(layout_dir) = workspace_oci_layout_dir(base, workspace_folder) else {
return Vec::new();
Expand Down Expand Up @@ -463,6 +453,18 @@ fn fixture_catalog() -> Vec<(String, CatalogEntry)> {
depends_on: Some(vec!["ghcr.io/codspace/dependson/E".to_string()]),
},
),
(
"https://github.com/codspace/tgz-features-with-dependson/releases/download/0.0.2/devcontainer-feature-B.tgz".to_string(),
CatalogEntry {
version: "0.0.2".to_string(),
resolved: "https://github.com/codspace/tgz-features-with-dependson/releases/download/0.0.2/devcontainer-feature-B.tgz".to_string(),
integrity: "sha256:d130123ba54335a026ab6cd51c8bcbd52d58a0aeaacd8a593512ba61c5117ea0".to_string(),
depends_on: Some(vec![
"ghcr.io/codspace/dependson/C".to_string(),
"ghcr.io/codspace/dependson/D".to_string(),
]),
},
),
]
}

Expand Down
1 change: 1 addition & 0 deletions cmd/devcontainer/src/commands/configuration/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ mod types;
pub(crate) use install::{feature_installation_name, materialize_feature_installation};
pub(crate) use metadata::apply_feature_metadata;
pub(crate) use resolve::resolve_feature_support;
pub(super) use resolve::resolve_feature_support_without_lockfile;
pub(crate) use types::{FeatureInstallation, ResolvedFeatureSupport};
Loading
Loading