diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 9d13b4f8d..0b15d85f8 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -2,13 +2,14 @@ set -eu -manifest_path="cmd/devcontainer/Cargo.toml" +echo "Running make rust-fmt..." +make rust-fmt -echo "Running cargo fmt -- --check..." -cargo fmt --manifest-path "$manifest_path" --all -- --check +echo "Running make rust-clippy..." +make rust-clippy -echo "Running cargo clippy -- -D warnings..." -cargo clippy --manifest-path "$manifest_path" -- -D warnings +echo "Running make rust-check..." +make rust-check -echo "Running cargo check..." -cargo check --manifest-path "$manifest_path" +echo "Running make rust-doc..." +make rust-doc diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5f983696d..598113d97 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,18 @@ updates: directory: "/" schedule: interval: "weekly" + + - package-ecosystem: "cargo" + directory: "/cmd/devcontainer" + schedule: + interval: "weekly" + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "npm" + directory: "/tap" + schedule: + interval: "weekly" diff --git a/.github/workflows/devcontainer-release.yml b/.github/workflows/devcontainer-release.yml index 05ae5bba8..a54dbceb1 100644 --- a/.github/workflows/devcontainer-release.yml +++ b/.github/workflows/devcontainer-release.yml @@ -37,7 +37,7 @@ jobs: if [[ -z "$latest_tag" ]]; then base_version="0.0.0" else - base_version="${latest_tag#${TAG_PREFIX}}" + base_version="${latest_tag#"$TAG_PREFIX"}" fi IFS='.' read -r major minor patch <<< "$base_version" diff --git a/.github/workflows/rust-port-convergence.yml b/.github/workflows/rust-port-convergence.yml index 641379847..cd6392c06 100644 --- a/.github/workflows/rust-port-convergence.yml +++ b/.github/workflows/rust-port-convergence.yml @@ -35,17 +35,32 @@ jobs: - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov + - name: Install cargo-deny + uses: taiki-e/install-action@cargo-deny + + - name: Workflow lint + run: make actionlint-check + + - name: Shell lint + run: make shellcheck + - name: Rust fmt - run: cargo fmt --manifest-path cmd/devcontainer/Cargo.toml --all -- --check + run: make rust-fmt - name: Rust clippy - run: cargo clippy --manifest-path cmd/devcontainer/Cargo.toml -- -D warnings + run: make rust-clippy - name: Rust check - run: cargo check --manifest-path cmd/devcontainer/Cargo.toml + run: make rust-check + + - name: Rust doc + run: make rust-doc - name: Rust tests - run: cargo test --manifest-path cmd/devcontainer/Cargo.toml + run: make rust-tests + + - name: Dependency policy + run: make cargo-deny-check - name: Build release binary run: cargo build --release --manifest-path cmd/devcontainer/Cargo.toml @@ -57,7 +72,7 @@ jobs: run: make pypi-wheel-smoke - name: Rust coverage - run: cargo llvm-cov --manifest-path cmd/devcontainer/Cargo.toml --lcov --output-path cmd/devcontainer/target/coverage.lcov + run: make rust-coverage - name: Native-only startup contract run: node build/check-native-only.js diff --git a/Makefile b/Makefile index 9b08a9f3c..14a857d9a 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,12 @@ rust-fmt \ rust-clippy \ rust-check \ + rust-doc \ rust-tests \ + cargo-deny-check \ + rust-coverage \ + actionlint-check \ + shellcheck \ build-release \ real-engine-lifecycle-smoke \ real-engine-lifecycle-smoke-docker \ @@ -11,7 +16,9 @@ pypi-wheel-smoke \ native-only-startup-contract \ acceptance-fixtures-check \ + check-upstream-submodule \ command-matrix-drift-check \ + check-cli-reference \ schema-drift-check \ parity-harness \ no-node-runtime \ @@ -25,27 +32,48 @@ check-cli-metadata \ check-compatibility-dashboard \ check-upstream-test-coverage \ + check-devcontainer-config \ upstream-compatibility RUST_MANIFEST := cmd/devcontainer/Cargo.toml RELEASE_BINARY := ./cmd/devcontainer/target/release/devcontainer +CARGO_LLVM_COV ?= cargo llvm-cov +COVERAGE_LINE_THRESHOLD := 88 +ACTIONLINT := uv tool run --from actionlint-py actionlint +SHELLCHECK := uv tool run --from shellcheck-py shellcheck +SHELLCHECK_FILES := $(shell git ls-files -- '*.sh' '.githooks/pre-commit' ':(exclude)upstream/**' ':(exclude)spec/**' ':(exclude)target/**' ':(exclude)node_modules/**') -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 tap-check homebrew-distribution-check npm-publish-workflow-check check-parity-inventory check-cli-metadata check-compatibility-dashboard check-upstream-test-coverage upstream-compatibility +tests: rust-fmt rust-tests rust-clippy rust-check rust-doc rust-coverage cargo-deny-check actionlint-check shellcheck build-release standalone-artifact-smoke pypi-wheel-smoke native-only-startup-contract acceptance-fixtures-check check-upstream-submodule command-matrix-drift-check check-cli-reference schema-drift-check parity-harness no-node-runtime npm-wrapper-check npm-publish-script-check npm-package-smoke tap-check homebrew-distribution-check npm-publish-workflow-check check-parity-inventory check-cli-metadata check-compatibility-dashboard check-upstream-test-coverage check-devcontainer-config upstream-compatibility rust-fmt: cargo fmt --manifest-path $(RUST_MANIFEST) --all -- --check rust-clippy: - cargo clippy --manifest-path $(RUST_MANIFEST) -- -D warnings + cargo clippy --manifest-path $(RUST_MANIFEST) --locked --all-targets --all-features -- -D warnings rust-check: - cargo check --manifest-path $(RUST_MANIFEST) + cargo check --manifest-path $(RUST_MANIFEST) --locked --all-targets --all-features + +rust-doc: + cargo doc --manifest-path $(RUST_MANIFEST) --locked --no-deps --document-private-items rust-tests: - cargo test --manifest-path $(RUST_MANIFEST) + cargo test --manifest-path $(RUST_MANIFEST) --locked + +cargo-deny-check: + cargo deny --manifest-path $(RUST_MANIFEST) check -A license-not-encountered + +rust-coverage: + $(CARGO_LLVM_COV) --manifest-path $(RUST_MANIFEST) --locked --all-features --workspace --fail-under-lines $(COVERAGE_LINE_THRESHOLD) + +actionlint-check: + $(ACTIONLINT) .github/workflows/*.yml + +shellcheck: + $(SHELLCHECK) $(SHELLCHECK_FILES) build-release: - cargo build --release --manifest-path $(RUST_MANIFEST) + cargo build --release --manifest-path $(RUST_MANIFEST) --locked real-engine-lifecycle-smoke: real-engine-lifecycle-smoke-docker real-engine-lifecycle-smoke-podman @@ -67,9 +95,15 @@ native-only-startup-contract: acceptance-fixtures-check: node build/check-acceptance-fixtures.js +check-upstream-submodule: + node build/check-upstream-submodule.js + command-matrix-drift-check: node build/generate-command-matrix.js --check +check-cli-reference: + node build/generate-cli-reference.js --check + schema-drift-check: node build/check-spec-drift.js @@ -110,5 +144,8 @@ check-compatibility-dashboard: check-upstream-test-coverage: node build/check-upstream-test-coverage.js +check-devcontainer-config: + node build/check-devcontainer-config.js + upstream-compatibility: node build/check-upstream-compatibility.js diff --git a/README.md b/README.md index a9379e43d..02c709403 100644 --- a/README.md +++ b/README.md @@ -60,18 +60,35 @@ If `upstream/` or `spec/` is missing or uninitialized, run the same command agai ## Local development +Run the complete local gate before pushing: + +```bash +make tests +``` + Rust validation: ```bash cargo fmt --manifest-path cmd/devcontainer/Cargo.toml --all -- --check -cargo clippy --manifest-path cmd/devcontainer/Cargo.toml -- -D warnings -cargo test --manifest-path cmd/devcontainer/Cargo.toml +cargo clippy --manifest-path cmd/devcontainer/Cargo.toml --all-targets --all-features -- -D warnings +cargo check --manifest-path cmd/devcontainer/Cargo.toml --all-targets --all-features +cargo doc --manifest-path cmd/devcontainer/Cargo.toml --no-deps --document-private-items +cargo test --manifest-path cmd/devcontainer/Cargo.toml --locked +cargo deny --manifest-path cmd/devcontainer/Cargo.toml check -A license-not-encountered +``` + +CI also enforces the current Rust line coverage baseline: + +```bash +cargo llvm-cov --manifest-path cmd/devcontainer/Cargo.toml --all-features --workspace --fail-under-lines 88 ``` Compatibility/tooling validation: ```bash npm test +make actionlint-check +make shellcheck ``` Manual acceptance suite shape: diff --git a/cmd/devcontainer/Cargo.toml b/cmd/devcontainer/Cargo.toml index e5a40d103..c51179bb1 100644 --- a/cmd/devcontainer/Cargo.toml +++ b/cmd/devcontainer/Cargo.toml @@ -17,6 +17,20 @@ serde_yaml = "0.9" sha2 = "0.10" tar = "0.4" +[lints.rust] +rust_2018_idioms = { level = "deny", priority = -1 } +unsafe_code = "forbid" +unreachable_pub = "warn" + +[lints.rustdoc] +bare_urls = "deny" +broken_intra_doc_links = "deny" + +[lints.clippy] +dbg_macro = "deny" +todo = "deny" +unimplemented = "deny" + [package.metadata.dist] dist = true diff --git a/cmd/devcontainer/src/commands/collections/tests/feature_tests.rs b/cmd/devcontainer/src/commands/collections/tests/feature_tests.rs index 33e0e28eb..6f74d12fe 100644 --- a/cmd/devcontainer/src/commands/collections/tests/feature_tests.rs +++ b/cmd/devcontainer/src/commands/collections/tests/feature_tests.rs @@ -10,17 +10,19 @@ use crate::commands::collections::feature_tests::{ const DEFAULT_FEATURE_TEST_BASE_IMAGE: &str = "docker.io/library/debian:bookworm-slim"; +type ExecCall = ( + String, + PathBuf, + Option, + Vec<(String, String)>, + String, +); + #[derive(Default)] struct FakeFeatureTestRuntime { build_calls: Vec<(String, PathBuf, PathBuf)>, start_calls: Vec<(String, PathBuf)>, - exec_calls: Vec<( - String, - PathBuf, - Option, - Vec<(String, String)>, - String, - )>, + exec_calls: Vec, remove_calls: Vec, } diff --git a/cmd/devcontainer/src/commands/configuration/catalog.rs b/cmd/devcontainer/src/commands/configuration/catalog.rs index cd172d061..7a307146a 100644 --- a/cmd/devcontainer/src/commands/configuration/catalog.rs +++ b/cmd/devcontainer/src/commands/configuration/catalog.rs @@ -532,6 +532,18 @@ impl VersionSelector { } } +impl Ord for ParsedVersion { + fn cmp(&self, other: &Self) -> Ordering { + (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch)) + } +} + +impl PartialOrd for ParsedVersion { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + #[cfg(test)] mod tests { use std::fs; @@ -561,7 +573,7 @@ mod tests { let metadata = json!({ "id": "published-feature", "version": version, - "dependsOn": depends_on.map(|entries| entries.iter().copied().collect::>()), + "dependsOn": depends_on.map(<[_]>::to_vec), }); let manifest = json!({ "schemaVersion": 2, @@ -731,15 +743,3 @@ mod tests { let _ = fs::remove_dir_all(workspace); } } - -impl Ord for ParsedVersion { - fn cmp(&self, other: &Self) -> Ordering { - (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch)) - } -} - -impl PartialOrd for ParsedVersion { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} diff --git a/cmd/devcontainer/src/commands/configuration/tests/upgrade.rs b/cmd/devcontainer/src/commands/configuration/tests/upgrade.rs index cdbe13149..fb785c7c8 100644 --- a/cmd/devcontainer/src/commands/configuration/tests/upgrade.rs +++ b/cmd/devcontainer/src/commands/configuration/tests/upgrade.rs @@ -357,7 +357,7 @@ fn write_workspace_layout_version( let metadata = json!({ "id": "published-feature", "version": version, - "dependsOn": depends_on.map(|entries| entries.iter().copied().collect::>()), + "dependsOn": depends_on.map(<[_]>::to_vec), }); let manifest = json!({ "schemaVersion": 2, diff --git a/cmd/devcontainer/src/lib.rs b/cmd/devcontainer/src/lib.rs index e9d266dd5..3efd1113f 100644 --- a/cmd/devcontainer/src/lib.rs +++ b/cmd/devcontainer/src/lib.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + //! Crate entry points and shared module wiring for the native devcontainer CLI. use std::env; @@ -18,16 +20,15 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub fn native_only_mode_enabled() -> bool { env::var(NATIVE_ONLY_ENV_VAR) - .map(|value| { - let normalized = value.trim().to_ascii_lowercase(); - !normalized.is_empty() - && normalized != "0" - && normalized != "false" - && normalized != "no" - }) + .map(|value| native_only_mode_value_enabled(&value)) .unwrap_or(false) } +fn native_only_mode_value_enabled(value: &str) -> bool { + let normalized = value.trim().to_ascii_lowercase(); + !normalized.is_empty() && normalized != "0" && normalized != "false" && normalized != "no" +} + pub fn run_from_env() -> ExitCode { run(env::args().skip(1).collect()) } @@ -110,21 +111,15 @@ pub fn run(raw_args: Vec) -> ExitCode { #[cfg(test)] mod tests { - use super::native_only_mode_enabled; + use super::native_only_mode_value_enabled; #[test] fn native_only_mode_uses_environment_switch() { - let original = std::env::var("DEVCONTAINER_NATIVE_ONLY").ok(); - std::env::set_var("DEVCONTAINER_NATIVE_ONLY", "1"); - assert!(native_only_mode_enabled()); - - std::env::set_var("DEVCONTAINER_NATIVE_ONLY", "false"); - assert!(!native_only_mode_enabled()); - - if let Some(value) = original { - std::env::set_var("DEVCONTAINER_NATIVE_ONLY", value); - } else { - std::env::remove_var("DEVCONTAINER_NATIVE_ONLY"); - } + assert!(native_only_mode_value_enabled("1")); + assert!(native_only_mode_value_enabled("yes")); + assert!(!native_only_mode_value_enabled("")); + assert!(!native_only_mode_value_enabled("0")); + assert!(!native_only_mode_value_enabled("false")); + assert!(!native_only_mode_value_enabled("no")); } } diff --git a/cmd/devcontainer/src/main.rs b/cmd/devcontainer/src/main.rs index 0cbc63bbc..e86f81859 100644 --- a/cmd/devcontainer/src/main.rs +++ b/cmd/devcontainer/src/main.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + //! Binary entrypoint for the native devcontainer CLI. fn main() -> std::process::ExitCode { diff --git a/cmd/devcontainer/src/runtime/compose/project.rs b/cmd/devcontainer/src/runtime/compose/project.rs index 38d1b7d31..fb008e00e 100644 --- a/cmd/devcontainer/src/runtime/compose/project.rs +++ b/cmd/devcontainer/src/runtime/compose/project.rs @@ -77,6 +77,13 @@ pub(super) fn compose_name_from_file(compose_file: &Path) -> Result String { + substitute_compose_env_with(value, &|name| env::var(name).ok()) +} + +pub(super) fn substitute_compose_env_with( + value: &str, + lookup: &dyn Fn(&str) -> Option, +) -> String { let trimmed = value.trim_matches('"').trim_matches('\''); let characters = trimmed.chars().collect::>(); let mut output = String::with_capacity(trimmed.len()); @@ -106,6 +113,7 @@ pub(super) fn substitute_compose_env(value: &str) -> String { } output.push_str(&expand_compose_variable( &characters[index + 2..end].iter().collect::(), + lookup, )); index = end + 1; continue; @@ -127,6 +135,7 @@ pub(super) fn substitute_compose_env(value: &str) -> String { } output.push_str(&expand_compose_variable( &characters[index + 1..end].iter().collect::(), + lookup, )); index = end; } @@ -134,21 +143,21 @@ pub(super) fn substitute_compose_env(value: &str) -> String { output } -fn expand_compose_variable(expression: &str) -> String { +fn expand_compose_variable(expression: &str, lookup: &dyn Fn(&str) -> Option) -> String { if let Some((name, default)) = expression.split_once(":-") { - return match env::var(name) { - Ok(value) if !value.is_empty() => value, - _ => substitute_compose_env(default), + return match lookup(name) { + Some(value) if !value.is_empty() => value, + _ => substitute_compose_env_with(default, lookup), }; } if let Some((name, default)) = expression.split_once('-') { - return match env::var(name) { - Ok(value) => value, - Err(_) => substitute_compose_env(default), + return match lookup(name) { + Some(value) => value, + None => substitute_compose_env_with(default, lookup), }; } - env::var(expression).unwrap_or_default() + lookup(expression).unwrap_or_default() } pub(super) fn sanitize_project_name(value: &str) -> String { diff --git a/cmd/devcontainer/src/runtime/compose/tests.rs b/cmd/devcontainer/src/runtime/compose/tests.rs index 4184ea573..f15e2121f 100644 --- a/cmd/devcontainer/src/runtime/compose/tests.rs +++ b/cmd/devcontainer/src/runtime/compose/tests.rs @@ -6,7 +6,8 @@ use std::fs; use super::build_service; use super::override_file::compose_metadata_override_file; use super::project::{ - compose_name_from_file, compose_project_name, sanitize_project_name, substitute_compose_env, + compose_name_from_file, compose_project_name, sanitize_project_name, + substitute_compose_env_with, }; use super::service::{ compose_image_name_separator, inspect_service_definition, parse_semver_prefix, @@ -232,19 +233,13 @@ fn compose_name_from_file_supports_dash_default_interpolation() { #[test] fn substitute_compose_env_supports_plain_variable_interpolation() { - let variable = format!("DEVCONTAINER_COMPOSE_TEST_PRESENT_{}", std::process::id()); - unsafe { - std::env::set_var(&variable, "MyProject"); - } + let variable = "DEVCONTAINER_COMPOSE_TEST_PRESENT"; + let lookup = |name: &str| (name == variable).then_some("MyProject".to_string()); assert_eq!( - substitute_compose_env(&format!("prefix-${variable}")), + substitute_compose_env_with(&format!("prefix-${variable}"), &lookup), "prefix-MyProject" ); - - unsafe { - std::env::remove_var(variable); - } } #[test] diff --git a/cmd/devcontainer/tests/cli_smoke/lockfile.rs b/cmd/devcontainer/tests/cli_smoke/lockfile.rs index 5e8cd25ab..a5eb7df8e 100644 --- a/cmd/devcontainer/tests/cli_smoke/lockfile.rs +++ b/cmd/devcontainer/tests/cli_smoke/lockfile.rs @@ -489,7 +489,7 @@ fn write_workspace_layout_version( let metadata = json!({ "id": "published-feature", "version": version, - "dependsOn": depends_on.map(|entries| entries.iter().copied().collect::>()), + "dependsOn": depends_on.map(<[_]>::to_vec), }); let manifest = json!({ "schemaVersion": 2, diff --git a/cmd/devcontainer/tests/cli_smoke/version.rs b/cmd/devcontainer/tests/cli_smoke/version.rs index 8d327ab68..4c57b415a 100644 --- a/cmd/devcontainer/tests/cli_smoke/version.rs +++ b/cmd/devcontainer/tests/cli_smoke/version.rs @@ -6,19 +6,17 @@ use crate::support::test_support::devcontainer_command; #[test] fn top_level_version_flags_print_the_package_version() { - for args in [["--version"]] { - let output = devcontainer_command(None) - .args(args) - .output() - .expect("version command should run"); + let output = devcontainer_command(None) + .arg("--version") + .output() + .expect("version command should run"); - assert!(output.status.success(), "{output:?}"); - assert_eq!( - String::from_utf8(output.stdout).expect("utf8 stdout"), - format!("{VERSION}\n") - ); - assert_eq!(String::from_utf8(output.stderr).expect("utf8 stderr"), ""); - } + assert!(output.status.success(), "{output:?}"); + assert_eq!( + String::from_utf8(output.stdout).expect("utf8 stdout"), + format!("{VERSION}\n") + ); + assert_eq!(String::from_utf8(output.stderr).expect("utf8 stderr"), ""); } #[test] diff --git a/cmd/devcontainer/tests/support/mod.rs b/cmd/devcontainer/tests/support/mod.rs index 871b12c8e..8de0dbc4b 100644 --- a/cmd/devcontainer/tests/support/mod.rs +++ b/cmd/devcontainer/tests/support/mod.rs @@ -1,5 +1,5 @@ //! Shared integration-test support modules. -pub mod runtime_harness; -pub mod test_support; -pub mod workspace_fixture; +pub(crate) mod runtime_harness; +pub(crate) mod test_support; +pub(crate) mod workspace_fixture; diff --git a/cmd/devcontainer/tests/support/runtime_harness.rs b/cmd/devcontainer/tests/support/runtime_harness.rs index 110b59017..1be4884e0 100644 --- a/cmd/devcontainer/tests/support/runtime_harness.rs +++ b/cmd/devcontainer/tests/support/runtime_harness.rs @@ -14,14 +14,14 @@ use super::test_support::{devcontainer_command, unique_temp_dir}; use super::workspace_fixture::WorkspaceFixture; use fake_engine::write_fake_podman; -pub struct RuntimeHarness { - pub root: PathBuf, - pub log_dir: PathBuf, - pub fake_podman: PathBuf, +pub(crate) struct RuntimeHarness { + pub(crate) root: PathBuf, + pub(crate) log_dir: PathBuf, + pub(crate) fake_podman: PathBuf, } impl RuntimeHarness { - pub fn new() -> Self { + pub(crate) fn new() -> Self { let root = unique_temp_dir("devcontainer-runtime-smoke"); let log_dir = root.join("logs"); fs::create_dir_all(&log_dir).expect("log dir"); @@ -34,15 +34,20 @@ impl RuntimeHarness { } } - pub fn workspace(&self) -> PathBuf { + pub(crate) fn workspace(&self) -> PathBuf { self.root.join("workspace") } - pub fn run(&self, args: &[&str], envs: &[(&str, &str)]) -> Output { + pub(crate) fn run(&self, args: &[&str], envs: &[(&str, &str)]) -> Output { self.run_in_dir(args, envs, None) } - pub fn run_in_dir(&self, args: &[&str], envs: &[(&str, &str)], cwd: Option<&Path>) -> Output { + pub(crate) fn run_in_dir( + &self, + args: &[&str], + envs: &[(&str, &str)], + cwd: Option<&Path>, + ) -> Output { let mut command = command(args, cwd); command.env( "FAKE_PODMAN_LOG_DIR", @@ -54,7 +59,12 @@ impl RuntimeHarness { command.output().expect("command should run") } - pub fn run_with_input(&self, args: &[&str], envs: &[(&str, &str)], input: &str) -> Output { + pub(crate) fn run_with_input( + &self, + args: &[&str], + envs: &[(&str, &str)], + input: &str, + ) -> Output { let mut command = command(args, None); command.stdin(Stdio::piped()); command.stdout(Stdio::piped()); @@ -77,29 +87,29 @@ impl RuntimeHarness { child.wait_with_output().expect("command should complete") } - pub fn read_invocations(&self) -> String { + pub(crate) fn read_invocations(&self) -> String { fs::read_to_string(self.log_dir.join("invocations.log")).expect("invocations") } - pub fn read_exec_log(&self) -> String { + pub(crate) fn read_exec_log(&self) -> String { fs::read_to_string(self.log_dir.join("exec.log")).expect("exec log") } - pub fn read_exec_argv_log(&self) -> String { + pub(crate) fn read_exec_argv_log(&self) -> String { fs::read_to_string(self.log_dir.join("exec-argv.log")).expect("exec argv log") } - pub fn read_compose_file_log(&self) -> String { + pub(crate) fn read_compose_file_log(&self) -> String { fs::read_to_string(self.log_dir.join("compose-file-contents.log")) .expect("compose file log") } - pub fn parse_stdout_json(&self, output: &Output) -> Value { + pub(crate) fn parse_stdout_json(&self, output: &Output) -> Value { serde_json::from_slice(&output.stdout).expect("json payload") } } -pub fn write_devcontainer_config(root: &Path, body: &str) -> PathBuf { +pub(crate) fn write_devcontainer_config(root: &Path, body: &str) -> PathBuf { WorkspaceFixture::new(root.to_path_buf()).write_devcontainer_config(body) } diff --git a/cmd/devcontainer/tests/support/test_support.rs b/cmd/devcontainer/tests/support/test_support.rs index 7d93a6cc4..43d7ef8d9 100644 --- a/cmd/devcontainer/tests/support/test_support.rs +++ b/cmd/devcontainer/tests/support/test_support.rs @@ -10,7 +10,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; static NEXT_TEMP_DIR_ID: AtomicU64 = AtomicU64::new(0); -pub fn unique_temp_dir(prefix: &str) -> PathBuf { +pub(crate) fn unique_temp_dir(prefix: &str) -> PathBuf { let suffix = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("time went backwards") @@ -22,7 +22,7 @@ pub fn unique_temp_dir(prefix: &str) -> PathBuf { )) } -pub fn repo_root() -> PathBuf { +pub(crate) fn repo_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .join("..") .join("..") @@ -30,7 +30,7 @@ pub fn repo_root() -> PathBuf { .expect("repo root") } -pub fn copy_recursive(source: &Path, destination: &Path) { +pub(crate) fn copy_recursive(source: &Path, destination: &Path) { let metadata = fs::metadata(source).expect("metadata"); if metadata.is_dir() { fs::create_dir_all(destination).expect("create dir"); @@ -46,7 +46,7 @@ pub fn copy_recursive(source: &Path, destination: &Path) { } } -pub fn devcontainer_command(cwd: Option<&Path>) -> Command { +pub(crate) fn devcontainer_command(cwd: Option<&Path>) -> Command { let mut command = Command::new(env!("CARGO_BIN_EXE_devcontainer")); if let Some(cwd) = cwd { command.current_dir(cwd); diff --git a/cmd/devcontainer/tests/support/workspace_fixture.rs b/cmd/devcontainer/tests/support/workspace_fixture.rs index 808447394..deb9fe77d 100644 --- a/cmd/devcontainer/tests/support/workspace_fixture.rs +++ b/cmd/devcontainer/tests/support/workspace_fixture.rs @@ -6,25 +6,25 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; -pub struct WorkspaceFixture { +pub(crate) struct WorkspaceFixture { root: PathBuf, } impl WorkspaceFixture { - pub fn new(root: PathBuf) -> Self { + pub(crate) fn new(root: PathBuf) -> Self { fs::create_dir_all(&root).expect("workspace dir"); Self { root } } - pub fn root(&self) -> &Path { + pub(crate) fn root(&self) -> &Path { &self.root } - pub fn config_dir(&self) -> PathBuf { + pub(crate) fn config_dir(&self) -> PathBuf { self.root.join(".devcontainer") } - pub fn write_devcontainer_config(&self, body: &str) -> PathBuf { + pub(crate) fn write_devcontainer_config(&self, body: &str) -> PathBuf { let config_dir = self.config_dir(); fs::create_dir_all(&config_dir).expect("config dir"); let config_path = config_dir.join("devcontainer.json"); @@ -32,13 +32,13 @@ impl WorkspaceFixture { config_path } - pub fn create_dir(&self, relative: &str) -> PathBuf { + pub(crate) fn create_dir(&self, relative: &str) -> PathBuf { let path = self.root.join(relative); fs::create_dir_all(&path).expect("dir"); path } - pub fn write_file(&self, relative: &str, contents: &str) -> PathBuf { + pub(crate) fn write_file(&self, relative: &str, contents: &str) -> PathBuf { let path = self.root.join(relative); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("parent dir"); @@ -47,7 +47,7 @@ impl WorkspaceFixture { path } - pub fn create_local_feature( + pub(crate) fn create_local_feature( &self, name: &str, manifest: &str, @@ -59,7 +59,7 @@ impl WorkspaceFixture { feature_dir } - pub fn init_dotfiles_repo(&self, relative: &str, install_script: &str) -> PathBuf { + pub(crate) fn init_dotfiles_repo(&self, relative: &str, install_script: &str) -> PathBuf { let repo = self.create_dir(relative); fs::write(repo.join("install.sh"), install_script).expect("install script"); run_git(&repo, &["init", "-q"]); @@ -70,13 +70,13 @@ impl WorkspaceFixture { repo } - pub fn init_git_repo(&self, relative: &str) -> PathBuf { + pub(crate) fn init_git_repo(&self, relative: &str) -> PathBuf { let repo = self.create_dir(relative); run_git(&repo, &["init", "--quiet"]); repo } - pub fn init_git_repo_with_commit(&self, relative: &str) -> PathBuf { + pub(crate) fn init_git_repo_with_commit(&self, relative: &str) -> PathBuf { let repo = self.init_git_repo(relative); fs::write(repo.join("README.md"), "hello\n").expect("readme"); run_git(&repo, &["add", "README.md"]); @@ -96,7 +96,7 @@ impl WorkspaceFixture { repo } - pub fn add_relative_git_worktree( + pub(crate) fn add_relative_git_worktree( &self, repo_relative: &str, worktree_relative: &str, diff --git a/deny.toml b/deny.toml new file mode 100644 index 000000000..023d23e84 --- /dev/null +++ b/deny.toml @@ -0,0 +1,33 @@ +[graph] +all-features = true + +[advisories] +ignore = [] + +[licenses] +allow = [ + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "MIT", + "Unicode-3.0", +] +confidence-threshold = 0.8 + +[bans] +multiple-versions = "warn" +wildcards = "deny" +skip = [] +skip-tree = [] + +[sources] +allow-git = [] +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +unknown-git = "deny" +unknown-registry = "deny" + +[sources.allow-org] +bitbucket = [] +github = [] +gitlab = [] diff --git a/docs/standalone/cutover.md b/docs/standalone/cutover.md index d6b4501a6..cc1881af3 100644 --- a/docs/standalone/cutover.md +++ b/docs/standalone/cutover.md @@ -7,13 +7,23 @@ ## Active guardrails - `cargo fmt --manifest-path cmd/devcontainer/Cargo.toml --all -- --check` -- `cargo clippy --manifest-path cmd/devcontainer/Cargo.toml -- -D warnings` -- `cargo test --manifest-path cmd/devcontainer/Cargo.toml` +- `cargo clippy --manifest-path cmd/devcontainer/Cargo.toml --all-targets --all-features -- -D warnings` +- `cargo check --manifest-path cmd/devcontainer/Cargo.toml --all-targets --all-features` +- `cargo doc --manifest-path cmd/devcontainer/Cargo.toml --no-deps --document-private-items` +- `cargo test --manifest-path cmd/devcontainer/Cargo.toml --locked` +- `cargo deny --manifest-path cmd/devcontainer/Cargo.toml check -A license-not-encountered` +- `cargo llvm-cov --manifest-path cmd/devcontainer/Cargo.toml --all-features --workspace --fail-under-lines 88` +- `uv tool run --from actionlint-py actionlint .github/workflows/*.yml` +- `uv tool run --from shellcheck-py shellcheck ` +- `node build/check-upstream-submodule.js` - `node build/check-native-only.js` - `node build/check-no-node-runtime.js` - `node build/check-parity-harness.js` +- `node build/generate-command-matrix.js --check` - `node build/generate-cli-reference.js --check` - `node build/generate-parity-inventory.js --check` +- `node build/check-spec-drift.js` +- `node build/check-devcontainer-config.js` ## Current parity scope diff --git a/scripts/install-git-hooks.sh b/scripts/install-git-hooks.sh index c69067207..36b3e7d2e 100755 --- a/scripts/install-git-hooks.sh +++ b/scripts/install-git-hooks.sh @@ -2,7 +2,10 @@ set -eu -repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +repo_root=$( + CDPATH= + cd -- "$(dirname -- "$0")/.." && pwd +) git -C "$repo_root" config core.hooksPath .githooks