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
29 changes: 29 additions & 0 deletions .devcontainer/Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,34 @@ RUN set -eux \
&& mkdir -p /workspaces \
&& chown -R "${USERNAME}:${USERNAME}" /workspaces

ARG GIT_VERSION=2.55.0
ARG GIT_SHA256=457fdb04dc8728e007d4688695e6912e6f680727920f2a40bf11eacc17505357
ARG UV_VERSION=0.12.3
ARG CARGO_DENY_VERSION=0.20.2
ARG CARGO_LLVM_COV_VERSION=0.8.7

RUN set -eux \
&& apt-get update \
&& apt-get install --yes --no-install-recommends \
libcurl4-openssl-dev \
libexpat1-dev \
libssl-dev \
zlib1g-dev \
&& curl --fail --silent --show-error --location \
"https://mirrors.edge.kernel.org/pub/software/scm/git/git-${GIT_VERSION}.tar.xz" \
--output /tmp/git.tar.xz \
&& echo "${GIT_SHA256} /tmp/git.tar.xz" | sha256sum --check - \
&& tar --extract --xz --file /tmp/git.tar.xz --directory /tmp \
&& make -C "/tmp/git-${GIT_VERSION}" prefix=/usr/local NO_GETTEXT=YesPlease NO_TCLTK=YesPlease -j"$(nproc)" all \
&& make -C "/tmp/git-${GIT_VERSION}" prefix=/usr/local NO_GETTEXT=YesPlease NO_TCLTK=YesPlease install \
&& rm -rf /tmp/git.tar.xz "/tmp/git-${GIT_VERSION}" \
&& rm -rf /var/lib/apt/lists/* \
&& curl --fail --silent --show-error --location "https://astral.sh/uv/${UV_VERSION}/install.sh" \
| env UV_INSTALL_DIR=/usr/local/bin sh \
&& rustup component add llvm-tools-preview \
&& cargo install --locked --version "${CARGO_DENY_VERSION}" cargo-deny \
&& cargo install --locked --version "${CARGO_LLVM_COV_VERSION}" cargo-llvm-cov \
&& chown -R "${USERNAME}:${USERNAME}" /usr/local/cargo /usr/local/rustup

USER ${USERNAME}
WORKDIR /workspaces/devcontainer-rs
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"context": "..",
"dockerfile": "Containerfile"
},
"postCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && git submodule update --init --recursive",
"postCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && git submodule update --init --recursive && COREPACK_ENABLE_PROJECT_SPEC=0 corepack yarn --cwd upstream install --frozen-lockfile --modules-folder ../node_modules",
"remoteUser": "dev",
"updateRemoteUserUID": true
}
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
check-compatibility-dashboard \
check-upstream-test-coverage \
check-devcontainer-config \
devcontainer-provision-smoke \
upstream-compatibility

RUST_MANIFEST := cmd/devcontainer/Cargo.toml
Expand All @@ -43,7 +44,7 @@ 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-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 artifact-smoke-workflow-check 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
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 artifact-smoke-workflow-check 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 devcontainer-provision-smoke upstream-compatibility

rust-fmt:
cargo fmt --manifest-path $(RUST_MANIFEST) --all -- --check
Expand Down Expand Up @@ -150,5 +151,8 @@ check-upstream-test-coverage:
check-devcontainer-config:
node build/check-devcontainer-config.js

devcontainer-provision-smoke: build-release
./scripts/standalone/devcontainer-provision-smoke.sh $(RELEASE_BINARY)

upstream-compatibility:
node build/check-upstream-compatibility.js
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ not shell command strings with arguments. Use
| `--buildkit` | `DEVCONTAINER_BUILDKIT` |
| `--user-data-folder` | `DEVCONTAINER_USER_DATA_FOLDER` |
| `--container-data-folder` | `DEVCONTAINER_CONTAINER_DATA_FOLDER` |
| `--dotfiles-repository` | `DEVCONTAINER_DOTFILES_REPOSITORY` |
| `--dotfiles-install-command` | `DEVCONTAINER_DOTFILES_INSTALL_COMMAND` |
| `--dotfiles-target-path` | `DEVCONTAINER_DOTFILES_TARGET_PATH` |
| `--gpu-availability` | `DEVCONTAINER_GPU_AVAILABILITY` |
| `--update-remote-user-uid-default` | `DEVCONTAINER_UPDATE_REMOTE_USER_UID_DEFAULT` |
| `--mount-workspace-git-root` | `DEVCONTAINER_MOUNT_WORKSPACE_GIT_ROOT` |
Expand Down
4 changes: 2 additions & 2 deletions build/check-devcontainer-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ function main() {
assert.equal(config.updateRemoteUserUID, true, 'devcontainer should keep host UID/GID alignment enabled');
assert.equal(
config.postCreateCommand,
'git config --global --add safe.directory ${containerWorkspaceFolder} && git submodule update --init --recursive',
'postCreateCommand should trust the mounted workspace before initializing pinned submodules',
'git config --global --add safe.directory ${containerWorkspaceFolder} && git submodule update --init --recursive && COREPACK_ENABLE_PROJECT_SPEC=0 corepack yarn --cwd upstream install --frozen-lockfile --modules-folder ../node_modules',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exercise devcontainer provisioning in the test gate

When the new Corepack/Yarn setup is invalid or fails to install usable upstream dependencies, this check still passes because it only compares the command text and never builds the devcontainer or executes postCreateCommand; the added Containerfile tools are likewise not exercised by this checker. Add a smoke test that creates the devcontainer, completes post-create setup, and invokes the required tools so make tests verifies the claimed behavior.

AGENTS.md reference: AGENTS.md:L51-L51

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex fix

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Added a real provisioning smoke test that clones the repository into an isolated workspace, creates the repository’s devcontainer, verifies successful lifecycle provisioning, and removes the container afterward. scripts/standalone/devcontainer-provision-smoke.shL9-L43
  • The smoke test executes inside the provisioned container and validates the installed upstream dependencies plus Git, Node.js, Corepack/Yarn, Rust, Cargo, cargo-deny, cargo-llvm-cov, and uv. scripts/standalone/devcontainer-provision-smoke.shL45-L59
  • Added devcontainer-provision-smoke to the documented phony targets and the full make tests verification gate. MakefileL25-L47
  • Configured the focused smoke target to build and exercise the release binary. MakefileL151-L155
  • Committed the changes as fa8b02f (Exercise devcontainer provisioning in test gate).
  • The pre-existing untracked package-lock.json was left untouched.
  • A pull request could not be created or published because this checkout has no origin remote and the required make_pr tool is unavailable in the environment.

Testing

  • ✅ node build/check-devcontainer-config.js
  • ✅ bash -n scripts/standalone/devcontainer-provision-smoke.sh
  • ✅ git diff --check
  • ⚠️ uv tool run --from shellcheck-py shellcheck scripts/standalone/devcontainer-provision-smoke.sh (the environment could not connect to PyPI to obtain shellcheck-py)
  • ⚠️ make devcontainer-provision-smoke (the release build passed, but Docker is not installed in this environment)
  • ❌ make tests (802 Rust tests passed, but the pre-existing commands::configuration::tests::upgrade::upgrade_feature_update_reports_config_write_errors test failed because it returned exit code 0 instead of the expected exit code 1)
  • ⚠️ git push origin HEAD (the checkout has no configured origin remote)
  • ⚠️ make_pr --title "Exercise devcontainer provisioning in test gate" ... (the required make_pr executable/tool is unavailable)

View task →

'postCreateCommand should initialize submodules and install pinned upstream tooling dependencies',
);
assert(!('features' in config), 'devcontainer should not depend on external devcontainer features');
assert(!('customizations' in config), 'devcontainer should not carry editor-specific customizations');
Expand Down
92 changes: 85 additions & 7 deletions cmd/devcontainer/src/commands/common/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ pub(crate) const DEVCONTAINER_CONFIG: &str = "DEVCONTAINER_CONFIG";
pub(crate) const DEVCONTAINER_BUILDKIT: &str = "DEVCONTAINER_BUILDKIT";
pub(crate) const DEVCONTAINER_USER_DATA_FOLDER: &str = "DEVCONTAINER_USER_DATA_FOLDER";
pub(crate) const DEVCONTAINER_CONTAINER_DATA_FOLDER: &str = "DEVCONTAINER_CONTAINER_DATA_FOLDER";
pub(crate) const DEVCONTAINER_DOTFILES_REPOSITORY: &str = "DEVCONTAINER_DOTFILES_REPOSITORY";
pub(crate) const DEVCONTAINER_DOTFILES_INSTALL_COMMAND: &str =
"DEVCONTAINER_DOTFILES_INSTALL_COMMAND";
pub(crate) const DEVCONTAINER_DOTFILES_TARGET_PATH: &str = "DEVCONTAINER_DOTFILES_TARGET_PATH";
pub(crate) const DEVCONTAINER_GPU_AVAILABILITY: &str = "DEVCONTAINER_GPU_AVAILABILITY";
pub(crate) const DEVCONTAINER_UPDATE_REMOTE_USER_UID_DEFAULT: &str =
"DEVCONTAINER_UPDATE_REMOTE_USER_UID_DEFAULT";
Expand Down Expand Up @@ -279,9 +283,21 @@ pub(crate) fn runtime_options(args: &[String]) -> RuntimeOptions {
"--update-remote-user-uid-default",
DEVCONTAINER_UPDATE_REMOTE_USER_UID_DEFAULT,
),
dotfiles_repository: parse_option_value(args, "--dotfiles-repository"),
dotfiles_install_command: parse_option_value(args, "--dotfiles-install-command"),
dotfiles_target_path: parse_option_value(args, "--dotfiles-target-path"),
dotfiles_repository: env_default_option_value(
args,
"--dotfiles-repository",
DEVCONTAINER_DOTFILES_REPOSITORY,
),
dotfiles_install_command: env_default_option_value(
args,
"--dotfiles-install-command",
DEVCONTAINER_DOTFILES_INSTALL_COMMAND,
),
dotfiles_target_path: env_default_option_value(
args,
"--dotfiles-target-path",
DEVCONTAINER_DOTFILES_TARGET_PATH,
),
user_data_folder: env_default_option_value(
args,
"--user-data-folder",
Expand Down Expand Up @@ -504,10 +520,11 @@ mod tests {
runtime_options, runtime_process_request, secrets_env, test_env_defaults,
validate_choice_option, validate_number_option, validate_option_values,
validate_paired_options, validate_runtime_env_defaults, DEVCONTAINER_BUILDKIT,
DEVCONTAINER_CONTAINER_DATA_FOLDER, DEVCONTAINER_GPU_AVAILABILITY,
DEVCONTAINER_MOUNT_GIT_WORKTREE_COMMON_DIR, DEVCONTAINER_MOUNT_WORKSPACE_GIT_ROOT,
DEVCONTAINER_UPDATE_REMOTE_USER_UID_DEFAULT, DEVCONTAINER_USER_DATA_FOLDER,
DEVCONTAINER_WORKSPACE_MOUNT_CONSISTENCY,
DEVCONTAINER_CONTAINER_DATA_FOLDER, DEVCONTAINER_DOTFILES_INSTALL_COMMAND,
DEVCONTAINER_DOTFILES_REPOSITORY, DEVCONTAINER_DOTFILES_TARGET_PATH,
DEVCONTAINER_GPU_AVAILABILITY, DEVCONTAINER_MOUNT_GIT_WORKTREE_COMMON_DIR,
DEVCONTAINER_MOUNT_WORKSPACE_GIT_ROOT, DEVCONTAINER_UPDATE_REMOTE_USER_UID_DEFAULT,
DEVCONTAINER_USER_DATA_FOLDER, DEVCONTAINER_WORKSPACE_MOUNT_CONSISTENCY,
};

#[test]
Expand Down Expand Up @@ -707,6 +724,9 @@ mod tests {
(DEVCONTAINER_BUILDKIT, "never"),
(DEVCONTAINER_GPU_AVAILABILITY, "all"),
(DEVCONTAINER_UPDATE_REMOTE_USER_UID_DEFAULT, "off"),
(DEVCONTAINER_DOTFILES_REPOSITORY, "owner/dotfiles"),
(DEVCONTAINER_DOTFILES_INSTALL_COMMAND, "bootstrap.sh"),
(DEVCONTAINER_DOTFILES_TARGET_PATH, "/env/dotfiles"),
(DEVCONTAINER_USER_DATA_FOLDER, "/env/user-data"),
(DEVCONTAINER_CONTAINER_DATA_FOLDER, "/env/container-data"),
]);
Expand All @@ -719,13 +739,71 @@ mod tests {
options.update_remote_user_uid_default.as_deref(),
Some("off")
);
assert_eq!(
options.dotfiles_repository.as_deref(),
Some("owner/dotfiles")
);
assert_eq!(
options.dotfiles_install_command.as_deref(),
Some("bootstrap.sh")
);
assert_eq!(
options.dotfiles_target_path.as_deref(),
Some("/env/dotfiles")
);
assert_eq!(options.user_data_folder.as_deref(), Some("/env/user-data"));
assert_eq!(
options.container_data_folder.as_deref(),
Some("/env/container-data")
);
}

#[test]
fn runtime_options_prefer_dotfiles_cli_values_over_env_defaults() {
let _env = test_env_defaults(&[
(DEVCONTAINER_DOTFILES_REPOSITORY, "env/repository"),
(DEVCONTAINER_DOTFILES_INSTALL_COMMAND, "env-install.sh"),
(DEVCONTAINER_DOTFILES_TARGET_PATH, "/env/dotfiles"),
]);

let options = runtime_options(&[
"--dotfiles-repository".to_string(),
"cli/repository".to_string(),
"--dotfiles-install-command".to_string(),
"cli-install.sh".to_string(),
"--dotfiles-target-path".to_string(),
"/cli/dotfiles".to_string(),
]);

assert_eq!(
options.dotfiles_repository.as_deref(),
Some("cli/repository")
);
assert_eq!(
options.dotfiles_install_command.as_deref(),
Some("cli-install.sh")
);
assert_eq!(
options.dotfiles_target_path.as_deref(),
Some("/cli/dotfiles")
);
}

#[test]
fn runtime_options_ignore_blank_dotfiles_env_defaults() {
let _env = test_env_defaults(&[
(DEVCONTAINER_DOTFILES_REPOSITORY, " "),
(DEVCONTAINER_DOTFILES_INSTALL_COMMAND, "\t"),
(DEVCONTAINER_DOTFILES_TARGET_PATH, "\n"),
]);

let options = runtime_options(&[]);

assert_eq!(options.dotfiles_repository, None);
assert_eq!(options.dotfiles_install_command, None);
assert_eq!(options.dotfiles_target_path, None);
}

#[test]
fn runtime_env_default_validation_checks_choices_and_bools() {
let env = test_env_defaults(&[(DEVCONTAINER_GPU_AVAILABILITY, "invalid")]);
Expand Down
48 changes: 48 additions & 0 deletions cmd/devcontainer/tests/runtime_lifecycle_smoke/dotfiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,54 @@ fn up_installs_dotfiles_between_post_create_and_post_start() {
assert!(exec_log.contains("printf 'post-start\\n' >> /workspaces/workspace/order.log"));
}

#[test]
fn up_installs_dotfiles_from_environment_defaults() {
let harness = RuntimeHarness::new();
let workspace = WorkspaceFixture::new(harness.workspace());
workspace.init_dotfiles_repo(
"dotfiles-env-repo",
"#!/bin/sh\nset -eu\nprintf 'env-dotfiles\\n' >> ../env-order.log\n",
);
let order_log = workspace.root().join("env-order.log");
write_devcontainer_config(
workspace.root(),
"{\n \"image\": \"alpine:3.20\",\n \"postCreateCommand\": \"printf 'post-create\\\\n' > /workspaces/workspace/env-order.log\",\n \"postStartCommand\": \"printf 'post-start\\\\n' >> /workspaces/workspace/env-order.log\"\n}\n",
);

let fake_podman = harness.fake_podman.to_string_lossy().to_string();
let output = harness.run(
&[
"up",
"--docker-path",
fake_podman.as_str(),
"--workspace-folder",
workspace.root().to_string_lossy().as_ref(),
"--container-data-folder",
"./.devcontainer-data",
],
&[
("DEVCONTAINER_DOTFILES_REPOSITORY", "./dotfiles-env-repo"),
("DEVCONTAINER_DOTFILES_INSTALL_COMMAND", "install.sh"),
(
"DEVCONTAINER_DOTFILES_TARGET_PATH",
"./env-applied-dotfiles",
),
("FAKE_PODMAN_PS_DISABLE_DEFAULT", "1"),
],
);

assert!(output.status.success(), "{output:?}");
assert_eq!(
fs::read_to_string(order_log).expect("environment order log"),
"post-create\nenv-dotfiles\npost-start\n"
);
let exec_log = harness.read_exec_log();
assert!(exec_log.contains("./dotfiles-env-repo"), "{exec_log}");
assert!(exec_log.contains("./env-applied-dotfiles"), "{exec_log}");
assert!(exec_log.contains("if [ -f './install.sh' ]"), "{exec_log}");
assert!(!exec_log.contains("for f in install.sh"), "{exec_log}");
}

#[test]
fn dotfiles_marker_skips_reinstall_on_followup_lifecycle_runs() {
let harness = RuntimeHarness::new();
Expand Down
59 changes: 59 additions & 0 deletions scripts/standalone/devcontainer-provision-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail

if [[ $# -ne 1 || ! -x "$1" ]]; then
echo "usage: $0 <standalone-binary-path>" >&2
exit 2
fi

binary="$1"
repository_root="$(git rev-parse --show-toplevel)"
tmp_dir="$(mktemp -d)"
workspace="$tmp_dir/workspace"
result_file="$tmp_dir/up.json"
container_id=""

cleanup() {
if [[ -n "$container_id" ]]; then
docker rm --force "$container_id" >/dev/null 2>&1 || true
fi
rm -rf "$tmp_dir"
}
trap cleanup EXIT

if ! command -v docker >/dev/null 2>&1; then
echo "docker is required for the devcontainer provisioning smoke test" >&2
exit 2
fi

git clone --quiet --local --no-hardlinks "$repository_root" "$workspace"

"$binary" up --workspace-folder "$workspace" >"$result_file"
if ! grep -Fq '"outcome":"success"' "$result_file"; then
echo "devcontainer creation did not report success" >&2
cat "$result_file" >&2
exit 1
fi

container_id="$(sed -n 's/.*"containerId":"\([^"]*\)".*/\1/p' "$result_file")"
if [[ -z "$container_id" ]]; then
echo "devcontainer creation did not return a container id" >&2
cat "$result_file" >&2
exit 1
fi

"$binary" exec --workspace-folder "$workspace" /bin/bash -lc '
set -euo pipefail
test -d node_modules
git --version
node --version
corepack --version
COREPACK_ENABLE_PROJECT_SPEC=0 corepack yarn --cwd upstream --version
rustc --version
cargo --version
cargo deny --version
cargo llvm-cov --version
uv --version
'

echo "[devcontainer-provision] devcontainer built, provisioned, and exposed the required tools."
Loading