From 196bf76c5f9de6ade113bfe711ac9d7b4212de16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:01:04 +0000 Subject: [PATCH 1/4] fix(containerise): skip per-build wrapper prune when NESTED to prevent devcontainer cache miss --- scripts/containerise.sh | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/scripts/containerise.sh b/scripts/containerise.sh index de5c218..7bd4dfd 100755 --- a/scripts/containerise.sh +++ b/scripts/containerise.sh @@ -1212,17 +1212,19 @@ build_base_image() { print_info "Use in other projects: ${CYAN}dc-contain --img --$category${NC}" echo "" # Prune stale VS Code UID-wrapper images (vsc-*-uid). VS Code names these by a hash of devcontainer.json content, not the image digest, so Podman's layer cache resolves the FROM mutable tag to the old digest on rebuild and the wrapper is silently built from the pre-fix base. Pruning forces VS Code to rebuild the wrapper from the new base on next open (~5s). - local stale_wrappers=() - mapfile -t stale_wrappers < <( - podman images --format "{{.Repository}}" 2>/dev/null \ - | grep "^localhost/vsc-" || true - ) - if [[ ${#stale_wrappers[@]} -gt 0 ]]; then - print_info "Pruning orphaned VS Code UID-wrapper image(s) (in-use wrappers are skipped by Podman)..." - for wrapper in "${stale_wrappers[@]}"; do - podman rmi "$wrapper" 2>/dev/null || true - done - print_success "Pruned orphaned wrappers. VS Code rebuilds them from the new base on next open." + if [[ "$NESTED" != "true" ]]; then + local stale_wrappers=() + mapfile -t stale_wrappers < <( + podman images --format "{{.Repository}}" 2>/dev/null \ + | grep "^localhost/vsc-" || true + ) + if [[ ${#stale_wrappers[@]} -gt 0 ]]; then + print_info "Pruning orphaned VS Code UID-wrapper image(s) (in-use wrappers are skipped by Podman)..." + for wrapper in "${stale_wrappers[@]}"; do + podman rmi "$wrapper" 2>/dev/null || true + done + print_success "Pruned orphaned wrappers. VS Code rebuilds them from the new base on next open." + fi fi else echo "" From cbda925047c1266f684a94d4ac6a8550dd7628f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:01:18 +0000 Subject: [PATCH 2/4] fix(containerise): pass --pull=true to podman build to match VS Code defaults and avoid cache miss --- scripts/containerise.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/containerise.sh b/scripts/containerise.sh index 7bd4dfd..906c1c3 100755 --- a/scripts/containerise.sh +++ b/scripts/containerise.sh @@ -1201,7 +1201,7 @@ build_base_image() { echo "" cd "$devcontainer_dir" - local build_args=() + local build_args=(--pull=true) [[ "$NO_CACHE" == "true" ]] && build_args+=("--no-cache") if podman build "${build_args[@]}" -t "$image_tag" .; then echo "" From 9a8db4bf9f9fea691dd6c25b738d4ce39a10b61e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:01:40 +0000 Subject: [PATCH 3/4] feat(containerise): add --prune mode with graceful state awareness to protect excluded projects during global prune --- scripts/containerise.sh | 188 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/scripts/containerise.sh b/scripts/containerise.sh index 906c1c3..f148e8c 100755 --- a/scripts/containerise.sh +++ b/scripts/containerise.sh @@ -51,6 +51,9 @@ NEST_MODE=false NEST_REGEN=false NO_CACHE=false ASSUME_YES=false +PRUNE_MODE=false +PRUNE_ALL=false +PRUNE_IMAGES=false EXCLUDE_DIRS=() ################################################################################ @@ -148,6 +151,19 @@ parse_args() { NEST_REGEN=true shift ;; + --prune) + PRUNE_MODE=true + USE_DEFAULTS=true + shift + ;; + -a|--all) + PRUNE_ALL=true + shift + ;; + --images) + PRUNE_IMAGES=true + shift + ;; --no-cache) NO_CACHE=true shift @@ -1339,6 +1355,173 @@ nest_path_is_excluded() { } +run_prune_mode() { + local start_dir="${1:-$(pwd)}" + print_header "Prune Mode" + + # Prune devcontainers under this start_dir + local -a all_container_ids + mapfile -t all_container_ids < <( + docker ps -q -a --filter "label=devcontainer.local_folder" 2>/dev/null | grep -v "^$" || true + ) + + local -a container_ids=() + local -a kept_container_ids=() + local container_id + for container_id in "${all_container_ids[@]}"; do + local folder + folder=$(docker inspect "$container_id" --format='{{index .Config.Labels "devcontainer.local_folder"}}' 2>/dev/null || echo "") + + # In prune mode, -a implies we prune ALL dev-control containers across the system, unless excluded. + if [[ "$PRUNE_ALL" != true && -n "$folder" && "$folder" != "$start_dir"* && "$folder" != "$start_dir" ]]; then + continue + fi + + if [[ ${#EXCLUDE_DIRS[@]} -gt 0 && -n "$folder" ]] && nest_path_is_excluded "$folder" "$start_dir"; then + kept_container_ids+=("$container_id") + else + container_ids+=("$container_id") + fi + done + + if [[ ${#kept_container_ids[@]} -gt 0 ]]; then + print_info "Excluding ${#kept_container_ids[@]} container(s) matched by --exclude (kept):" + for container_id in "${kept_container_ids[@]}"; do + local folder + folder=$(docker inspect "$container_id" --format='{{index .Config.Labels "devcontainer.local_folder"}}' 2>/dev/null || echo "") + echo " - ${folder:-unknown} (${container_id:0:12})" + done + echo "" + fi + + if [[ ${#container_ids[@]} -gt 0 ]]; then + print_info "Found ${#container_ids[@]} dev-control containers to delete:" + for container_id in "${container_ids[@]}"; do + local folder + folder=$(docker inspect "$container_id" --format='{{index .Config.Labels "devcontainer.local_folder"}}' 2>/dev/null || echo "") + echo " - ${folder:-unknown} (${container_id:0:12})" + done + echo "" + + if confirm "Delete these containers and associated volumes?"; then + echo "" + local -a keep_vol_names=() + for container_id in "${kept_container_ids[@]}"; do + mapfile -t -O "${#keep_vol_names[@]}" keep_vol_names < <( + docker inspect "$container_id" \ + --format='{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\n"}}{{end}}{{end}}' \ + 2>/dev/null | grep -v "^$" || true + ) + done + + local -a all_vol_names=() + for container_id in "${container_ids[@]}"; do + mapfile -t -O "${#all_vol_names[@]}" all_vol_names < <( + docker inspect "$container_id" \ + --format='{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\n"}}{{end}}{{end}}' \ + 2>/dev/null | grep -v "^$" || true + ) + done + + for container_id in "${container_ids[@]}"; do + print_info "Stopping container ${container_id:0:12}..." + docker stop "$container_id" 2>/dev/null || true + print_info "Removing container ${container_id:0:12}..." + docker rm "$container_id" 2>/dev/null || true + done + print_success "Deleted ${#container_ids[@]} containers" + + local -a vols_to_remove=() + local vol keep_vol is_kept + for vol in "${all_vol_names[@]}"; do + is_kept=false + for keep_vol in "${keep_vol_names[@]}"; do + [[ "$vol" == "$keep_vol" ]] && { is_kept=true; break; } + done + [[ "$is_kept" == false ]] && vols_to_remove+=("$vol") + done + + if [[ ${#vols_to_remove[@]} -gt 0 ]]; then + print_info "Removing ${#vols_to_remove[@]} associated volume(s)..." + docker volume rm "${vols_to_remove[@]}" 2>/dev/null || true + print_success "Removed volumes: ${vols_to_remove[*]}" + fi + fi + else + print_info "No dev-control containers found to delete." + echo "" + fi + + if [[ "$PRUNE_IMAGES" == true ]]; then + local -a excl_basenames=() + if [[ ${#EXCLUDE_DIRS[@]} -gt 0 ]]; then + local _ed + for _ed in "${EXCLUDE_DIRS[@]}"; do + excl_basenames+=( "$(basename "${_ed%/}" | tr '[:upper:]' '[:lower:]')" ) + done + fi + + # Prune wrapper images (localhost/vsc-*) + local stale_wrappers=() + mapfile -t stale_wrappers < <( + podman images --format "{{.Repository}}" 2>/dev/null \ + | grep "^localhost/vsc-" || true + ) + if [[ ${#stale_wrappers[@]} -gt 0 ]]; then + local -a wrappers_to_prune=() + local wrapper _bn keep_wrapper + for wrapper in "${stale_wrappers[@]}"; do + keep_wrapper=false + for _bn in "${excl_basenames[@]}"; do + [[ -n "$_bn" && "$wrapper" == "localhost/vsc-$_bn-"* ]] && { keep_wrapper=true; break; } + done + if [[ "$keep_wrapper" == true ]]; then + print_info "Excluding wrapper image (kept): $wrapper" + else + wrappers_to_prune+=("$wrapper") + fi + done + + if [[ ${#wrappers_to_prune[@]} -gt 0 ]]; then + if confirm "Prune ${#wrappers_to_prune[@]} VS Code UID-wrapper image(s)?"; then + for wrapper in "${wrappers_to_prune[@]}"; do + podman rmi "$wrapper" 2>/dev/null || true + done + print_success "Pruned VS Code UID-wrapper images." + fi + fi + fi + + if [[ "$PRUNE_ALL" == true ]]; then + if confirm "Run podman system prune -a --volumes (Warning: deletes all stopped containers and unused images globally)?"; then + # Graceful state awareness: start kept containers to protect them and their images from global prune + local -a started_for_protection=() + if [[ ${#kept_container_ids[@]} -gt 0 ]]; then + print_info "Temporarily starting ${#kept_container_ids[@]} excluded container(s) to protect them from global prune..." + for container_id in "${kept_container_ids[@]}"; do + local state + state=$(docker inspect "$container_id" --format='{{.State.Running}}' 2>/dev/null || echo "false") + if [[ "$state" != "true" ]]; then + docker start "$container_id" >/dev/null 2>&1 && started_for_protection+=("$container_id") || true + fi + done + fi + + print_info "Running global podman system prune..." + podman system prune -a --volumes -f + print_success "Global prune complete." + + if [[ ${#started_for_protection[@]} -gt 0 ]]; then + print_info "Stopping temporarily started containers..." + for container_id in "${started_for_protection[@]}"; do + docker stop "$container_id" >/dev/null 2>&1 || true + done + fi + fi + fi + fi + print_header_success "Prune complete" +} run_nest_mode() { local start_dir="${1:-$(pwd)}" local include_root=false @@ -1849,6 +2032,11 @@ main() { exit 0 fi + if [[ "$PRUNE_MODE" == true ]]; then + run_prune_mode "$PROJECT_PATH" + exit 0 + fi + # Handle --bare mode if [[ "$BARE_MODE" == true ]]; then print_header "Dev-Control Containerisation (Bare Mode)" From 27e627e83e992fa80951761ed9bb85c8586461d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:01:40 +0000 Subject: [PATCH 4/4] docs(changelog): record containerise cache fixes and new prune mode --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a96c98..add34be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [Unreleased] + +### Added +- `dc-contain --prune` mode with graceful state awareness to prune containers/images while protecting excluded projects by temporarily starting them. + +### Fixed +- `dc-contain`: Fix 1/33 devcontainer cache miss by passing `--pull=true` to podman build, matching VS Code DevContainers default behaviour. +- `dc-contain`: Fix cache invalidation by skipping per-build wrapper prune when `NESTED=true` during `--nest`. + All notable changes to Dev-Control will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),