From a8f0b45732c6b170aa48d9276c01da13be9d6e71 Mon Sep 17 00:00:00 2001 From: Brian Helba Date: Mon, 15 Jun 2026 08:31:24 -0400 Subject: [PATCH 01/21] [node] - Install pnpm as non-root user to prevent root-owned npm cache (#1625) * [node] - Install pnpm as non-root user to prevent root-owned npm cache Currently, the pnpm installation block runs `npm install -g pnpm` in a bare subshell as root, unlike every other npm/nvm operation in the script which uses `su ${USERNAME}`. This causes the npm cache directory to be created owned by `root:root`, leading to `EACCES` errors for the non-root user on subsequent npm operations. This is particularly reproducible on macOS with Rosetta 2 emulation, where the cache directory may not already exist from prior steps. Note, the explicit setting of proxy env vars (`http_proxy`, `https_proxy`, `no_proxy`) is likely a workaround for https://github.com/npm/cli/issues/6835. No other commands in this script use that workaround anymore, so this change uses the same `su` syntax as all other commands. * Bump Node.js version to 2.1.0 --------- Co-authored-by: Abdurrahmaan Iqbal --- src/node/devcontainer-feature.json | 2 +- src/node/install.sh | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/node/devcontainer-feature.json b/src/node/devcontainer-feature.json index 2e86262ae..5e0970b2d 100644 --- a/src/node/devcontainer-feature.json +++ b/src/node/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "node", - "version": "2.0.0", + "version": "2.1.0", "name": "Node.js (via nvm), yarn and pnpm.", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/node", "description": "Installs Node.js, nvm, yarn, pnpm, and needed dependencies.", diff --git a/src/node/install.sh b/src/node/install.sh index 20ea85463..0e277812e 100755 --- a/src/node/install.sh +++ b/src/node/install.sh @@ -482,13 +482,7 @@ if [ ! -z "${PNPM_VERSION}" ] && [ "${PNPM_VERSION}" = "none" ]; then echo "Ignoring installation of PNPM" else if bash -c ". '${NVM_DIR}/nvm.sh' && type npm >/dev/null 2>&1"; then - ( - . "${NVM_DIR}/nvm.sh" - [ ! -z "$http_proxy" ] && npm set proxy="$http_proxy" - [ ! -z "$https_proxy" ] && npm set https-proxy="$https_proxy" - [ ! -z "$no_proxy" ] && npm set noproxy="$no_proxy" - npm install -g pnpm@$PNPM_VERSION --force - ) + su ${USERNAME} -c "umask 0002 && . '${NVM_DIR}/nvm.sh' && npm install -g pnpm@${PNPM_VERSION} --force" else echo "Skip installing pnpm because npm is missing" fi From f851b4860e3e40842bfc4053c0fb45b5a7d91c9a Mon Sep 17 00:00:00 2001 From: Kaniska Date: Wed, 17 Jun 2026 22:22:11 +0530 Subject: [PATCH 02/21] [Docker-in-Docker] - Update docker-compose to the latest version (#1672) * [Docker-in-Docker] - Update docker-compose to the latest version * Implement review comment. --- src/docker-in-docker/README.md | 2 +- .../devcontainer-feature.json | 7 ++++--- src/docker-in-docker/install.sh | 2 +- .../docker_compose_latest_moby.sh | 16 +++++++++++++++ .../docker_compose_latest_no_moby.sh | 14 +++++++++++++ test/docker-in-docker/scenarios.json | 20 +++++++++++++++++++ 6 files changed, 56 insertions(+), 5 deletions(-) create mode 100755 test/docker-in-docker/docker_compose_latest_moby.sh create mode 100644 test/docker-in-docker/docker_compose_latest_no_moby.sh diff --git a/src/docker-in-docker/README.md b/src/docker-in-docker/README.md index c58283317..f94c9be51 100644 --- a/src/docker-in-docker/README.md +++ b/src/docker-in-docker/README.md @@ -18,7 +18,7 @@ Create child containers *inside* a container, independent from the host's docker | version | Select or enter a Docker/Moby Engine version. (Availability can vary by OS version.) | string | latest | | moby | Install OSS Moby build instead of Docker CE | boolean | true | | mobyBuildxVersion | Install a specific version of moby-buildx when using Moby | string | latest | -| dockerDashComposeVersion | Default version of Docker Compose (v1, v2 or none) | string | v2 | +| dockerDashComposeVersion | Default version of Docker Compose (v1, v2, latest or none) | string | latest | | azureDnsAutoDetection | Allow automatically setting the dockerd DNS server when the installation script detects it is running in Azure | boolean | true | | dockerDefaultAddressPool | Define default address pools for Docker networks. e.g. base=192.168.0.0/16,size=24 | string | - | | installDockerBuildx | Install Docker Buildx | boolean | true | diff --git a/src/docker-in-docker/devcontainer-feature.json b/src/docker-in-docker/devcontainer-feature.json index 0af78923e..6d7c0431a 100644 --- a/src/docker-in-docker/devcontainer-feature.json +++ b/src/docker-in-docker/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "docker-in-docker", - "version": "3.0.1", + "version": "3.1.0", "name": "Docker (Docker-in-Docker)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/docker-in-docker", "description": "Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.", @@ -29,11 +29,12 @@ "type": "string", "enum": [ "none", + "latest", "v1", "v2" ], - "default": "v2", - "description": "Default version of Docker Compose (v1, v2 or none)" + "default": "latest", + "description": "Default version of Docker Compose (v1, v2, latest or none)" }, "azureDnsAutoDetection": { "type": "boolean", diff --git a/src/docker-in-docker/install.sh b/src/docker-in-docker/install.sh index e9740efc5..70a187e28 100755 --- a/src/docker-in-docker/install.sh +++ b/src/docker-in-docker/install.sh @@ -11,7 +11,7 @@ DOCKER_VERSION="${VERSION:-"latest"}" # The Docker/Moby Engine + CLI should match in version USE_MOBY="${MOBY:-"true"}" MOBY_BUILDX_VERSION="${MOBYBUILDXVERSION:-"latest"}" -DOCKER_DASH_COMPOSE_VERSION="${DOCKERDASHCOMPOSEVERSION:-"v2"}" #v1, v2 or none +DOCKER_DASH_COMPOSE_VERSION="${DOCKERDASHCOMPOSEVERSION:-"latest"}" #v1, v2, latest or none AZURE_DNS_AUTO_DETECTION="${AZUREDNSAUTODETECTION:-"true"}" DOCKER_DEFAULT_ADDRESS_POOL="${DOCKERDEFAULTADDRESSPOOL:-""}" USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}" diff --git a/test/docker-in-docker/docker_compose_latest_moby.sh b/test/docker-in-docker/docker_compose_latest_moby.sh new file mode 100755 index 000000000..bd5a9d23f --- /dev/null +++ b/test/docker-in-docker/docker_compose_latest_moby.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Definition specific tests +check "docker compose" bash -c "docker compose version | grep -E '[0-9]+\.[0-9]+\.[0-9]+'" +check "docker-compose" bash -c "docker-compose --version | grep -E '[0-9]+\.[0-9]+\.[0-9]+'" +check "installs compose as docker-compose" bash -c "[[ -f /usr/local/bin/docker-compose ]]" +check "moby-engine" bash -c "dpkg-query -W moby-engine" +check "moby-cli" bash -c "dpkg-query -W moby-cli" + +# Report result +reportResults diff --git a/test/docker-in-docker/docker_compose_latest_no_moby.sh b/test/docker-in-docker/docker_compose_latest_no_moby.sh new file mode 100644 index 000000000..5eeb34b45 --- /dev/null +++ b/test/docker-in-docker/docker_compose_latest_no_moby.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Definition specific tests +check "docker compose" bash -c "docker compose version | grep -E '[0-9]+\.[0-9]+\.[0-9]+'" +check "docker-compose" bash -c "docker-compose --version | grep -E '[0-9]+\.[0-9]+\.[0-9]+'" +check "installs compose as docker-compose" bash -c "[[ -f /usr/local/bin/docker-compose ]]" + +# Report result +reportResults diff --git a/test/docker-in-docker/scenarios.json b/test/docker-in-docker/scenarios.json index 2f9df3958..a93495b51 100644 --- a/test/docker-in-docker/scenarios.json +++ b/test/docker-in-docker/scenarios.json @@ -146,6 +146,26 @@ } } }, + "docker_compose_latest_moby": { + "image": "mcr.microsoft.com/devcontainers/base:noble", + "features": { + "docker-in-docker": { + "moby": true, + "installDockerBuildx": true, + "dockerDashComposeVersion": "latest" + } + } + }, + "docker_compose_latest_no_moby": { + "image": "mcr.microsoft.com/devcontainers/base:noble", + "features": { + "docker-in-docker": { + "moby": false, + "installDockerBuildx": true, + "dockerDashComposeVersion": "latest" + } + } + }, "docker_build_fallback_buildx": { "image": "ubuntu:noble", "features": { From 71d6d23dfb14f3bbae3de4080034002d57215adf Mon Sep 17 00:00:00 2001 From: Ryosuke Hiroe Date: Sat, 20 Jun 2026 02:24:36 +0900 Subject: [PATCH 03/21] feat(ruby)!: rewrite to use ruby-build with optional rbenv/rvm (v2.0.0) (#1654) * feat(ruby)!: rewrite to use ruby-build with optional rbenv/rvm (v2.0.0) Replace the rvm-only install with ruby-build under /usr/local/rubies, exposed via the 'current' symlink on the PATH. Add a versionManager option ("none" | "rbenv" | "rvm") that installs the chosen manager and delegates 'install' to it. Detect build deps across apt, dnf/yum, apk, zypper, and pacman. Replace ruby_fallback_test with ruby_rbenv / ruby_rvm scenarios. * test(ruby): symlink install_additional_ruby_trixie.sh to install_additional_ruby.sh --------- Co-authored-by: Kaniska --- src/ruby/NOTES.md | 9 +- src/ruby/README.md | 9 +- src/ruby/devcontainer-feature.json | 24 +- src/ruby/install.sh | 584 ++++++++++---------- test/ruby/install_additional_ruby.sh | 10 +- test/ruby/install_additional_ruby_trixie.sh | 18 +- test/ruby/install_ruby_trixie_base.sh | 8 +- test/ruby/ruby_fallback_test.sh | 310 ----------- test/ruby/ruby_rbenv.sh | 19 + test/ruby/ruby_rvm.sh | 19 + test/ruby/scenarios.json | 26 +- 11 files changed, 390 insertions(+), 646 deletions(-) mode change 100644 => 120000 test/ruby/install_additional_ruby_trixie.sh delete mode 100644 test/ruby/ruby_fallback_test.sh create mode 100755 test/ruby/ruby_rbenv.sh create mode 100755 test/ruby/ruby_rvm.sh diff --git a/src/ruby/NOTES.md b/src/ruby/NOTES.md index 19fe92f31..53da243c1 100644 --- a/src/ruby/NOTES.md +++ b/src/ruby/NOTES.md @@ -2,6 +2,13 @@ ## OS Support -This Feature should work on recent versions of Debian/Ubuntu-based distributions with the `apt` package manager installed. +This Feature supports Linux images that ship one of the following package managers: `apt`, `dnf`/`yum`, `apk`, `zypper`, or `pacman`. The script detects the available package manager and installs the build dependencies that ruby-build needs. `bash` is required to execute the `install.sh` script. + +## Layout + +- Ruby is installed under `/usr/local/rubies/` by ruby-build. +- The default Ruby is exposed via the `/usr/local/rubies/current` symlink, which is placed on the `PATH` through `containerEnv`. +- `ruby-build` itself is cloned to `/usr/local/share/ruby-build` and symlinked into `/usr/local/bin/ruby-build` so additional versions can be installed later. +- A shared `ruby` group owns `/usr/local/rubies`; the configured non-root user is added to it so `gem install` can write into the active Ruby tree without `sudo`. diff --git a/src/ruby/README.md b/src/ruby/README.md index 7df6965c7..351ffe906 100644 --- a/src/ruby/README.md +++ b/src/ruby/README.md @@ -1,13 +1,13 @@ -# Ruby (via rvm) (ruby) +# Ruby (via ruby-build) (ruby) -Installs Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies. +Installs Ruby using ruby-build, with optional rbenv or rvm for version management. ## Example Usage ```json "features": { - "ghcr.io/devcontainers/features/ruby:1": {} + "ghcr.io/devcontainers/features/ruby:2": {} } ``` @@ -16,6 +16,7 @@ Installs Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies. | Options Id | Description | Type | Default Value | |-----|-----|-----|-----| | version | Select or enter a Ruby version to install | string | latest | +| versionManager | Version manager to install alongside Ruby: 'rbenv', 'rvm', or 'none' (ruby-build only) | string | none | ## Customizations @@ -27,7 +28,7 @@ Installs Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies. ## OS Support -This Feature should work on recent versions of Debian/Ubuntu-based distributions with the `apt` package manager installed. +This Feature supports Linux images that ship one of the following package managers: `apt`, `dnf`/`yum`, `apk`, `zypper`, or `pacman`. The script detects the available package manager and installs the build dependencies that ruby-build needs. `bash` is required to execute the `install.sh` script. diff --git a/src/ruby/devcontainer-feature.json b/src/ruby/devcontainer-feature.json index 661c46bf0..841afbc3d 100644 --- a/src/ruby/devcontainer-feature.json +++ b/src/ruby/devcontainer-feature.json @@ -1,20 +1,26 @@ { "id": "ruby", - "version": "1.3.2", - "name": "Ruby (via rvm)", + "version": "2.0.0", + "name": "Ruby (via ruby-build)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/ruby", - "description": "Installs Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies.", + "description": "Installs Ruby using ruby-build, with optional rbenv or rvm for version management.", "options": { "version": { "type": "string", "proposals": [ "latest", "none", - "3.4", - "3.2" + "4.0", + "3.4" ], "default": "latest", "description": "Select or enter a Ruby version to install" + }, + "versionManager": { + "type": "string", + "enum": ["none", "rbenv", "rvm"], + "default": "none", + "description": "Version manager to install alongside Ruby: 'rbenv', 'rvm', or 'none' (ruby-build only)" } }, "customizations": { @@ -25,17 +31,15 @@ "settings": { "github.copilot.chat.codeGeneration.instructions": [ { - "text": "This dev container includes Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies pre-installed and available on the `PATH`, along with the Ruby language extension for Ruby development." + "text": "This dev container installs Ruby via ruby-build. rbenv or rvm may also be available depending on the versionManager option. The default Ruby is on the PATH via /usr/local/rubies/current/bin (or rbenv shims if rbenv is the version manager)." } ] } } }, "containerEnv": { - "GEM_PATH": "/usr/local/rvm/gems/default:/usr/local/rvm/gems/default@global", - "GEM_HOME": "/usr/local/rvm/gems/default", - "MY_RUBY_HOME": "/usr/local/rvm/rubies/default", - "PATH": "/usr/local/rvm/gems/default/bin:/usr/local/rvm/gems/default@global/bin:/usr/local/rvm/rubies/default/bin:/usr/local/share/rbenv/bin:${PATH}" + "RBENV_ROOT": "/usr/local/share/rbenv", + "PATH": "/usr/local/share/rbenv/shims:/usr/local/share/rbenv/bin:/usr/local/rubies/current/bin:${PATH}" }, "installsAfter": [ "ghcr.io/devcontainers/features/common-utils" diff --git a/src/ruby/install.sh b/src/ruby/install.sh index 39cb5be03..2b990cda8 100755 --- a/src/ruby/install.sh +++ b/src/ruby/install.sh @@ -10,24 +10,27 @@ RUBY_VERSION="${VERSION:-"latest"}" USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}" -UPDATE_RC="${UPDATE_RC:-"true"}" INSTALL_RUBY_TOOLS="${INSTALL_RUBY_TOOLS:-"true"}" -# Comma-separated list of ruby versions to be installed (with rvm) -# alongside RUBY_VERSION, but not set as default. +# Comma-separated list of ruby versions to be installed alongside RUBY_VERSION, +# but not set as default. ADDITIONAL_VERSIONS="${ADDITIONALVERSIONS:-""}" -# Note: ruby-debug-ide will install the right version of debase if missing and -# installing debase directly fails on Ruby 3.1.0 as of 1/7/2022, so omitting. -# installing ruby-debug-ide on debian fails, so omitting. +VERSION_MANAGER="${VERSIONMANAGER:-"none"}" + DEFAULT_GEMS="rake" +RUBY_BUILD_DIR="/usr/local/share/ruby-build" +RUBIES_DIR="/usr/local/rubies" +RUBY_GROUP="ruby" +RBENV_ROOT="/usr/local/share/rbenv" +RVM_PATH="/usr/local/rvm" RVM_GPG_KEYS="409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB" set -e -# Clean up -rm -rf /var/lib/apt/lists/* +# Force apt to refresh its lists below by clearing them up front (no-op on non-apt systems). +rm -rf /var/lib/apt/lists/* 2>/dev/null || true if [ "$(id -u)" -ne 0 ]; then echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' @@ -39,7 +42,6 @@ rm -f /etc/profile.d/00-restore-env.sh echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh chmod +x /etc/profile.d/00-restore-env.sh -# Determine the appropriate non-root user if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then USERNAME="" POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") @@ -56,19 +58,35 @@ elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then USERNAME=root fi -updaterc() { - if [ "${UPDATE_RC}" = "true" ]; then - echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." - if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then - echo -e "$1" >> /etc/bash.bashrc - fi - if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then - echo -e "$1" >> /etc/zsh/zshrc - fi +architecture="$(uname -m)" +if [ "${architecture}" != "amd64" ] && [ "${architecture}" != "x86_64" ] && [ "${architecture}" != "arm64" ] && [ "${architecture}" != "aarch64" ]; then + echo "(!) Architecture $architecture unsupported" + exit 1 +fi + +clone_or_update_repo() { + local repo=$1 dest=$2 + if [ ! -d "${dest}" ]; then + git clone --depth=1 \ + -c core.eol=lf \ + -c core.autocrlf=false \ + -c fsck.zeroPaddedFilemode=ignore \ + -c fetch.fsck.zeroPaddedFilemode=ignore \ + -c receive.fsck.zeroPaddedFilemode=ignore \ + "${repo}" "${dest}" + else + git -C "${dest}" fetch --depth=1 origin && \ + git -C "${dest}" reset --hard origin/HEAD || true fi } -# Get the list of GPG key servers that are reachable +apply_group_perms() { + local dir=$1 + chgrp -R "${RUBY_GROUP}" "${dir}" 2>/dev/null || true + chmod -R g+rw "${dir}" 2>/dev/null || true + find "${dir}" -type d -exec chmod g+s {} + 2>/dev/null || true +} + get_gpg_key_servers() { declare -A keyservers_curl_map=( ["hkp://keyserver.ubuntu.com"]="http://keyserver.ubuntu.com:11371" @@ -78,15 +96,15 @@ get_gpg_key_servers() { ) local curl_args="" - local keyserver_reachable=false # Flag to indicate if any keyserver is reachable + local keyserver_reachable=false - if [ ! -z "${KEYSERVER_PROXY}" ]; then + if [ -n "${KEYSERVER_PROXY:-}" ]; then curl_args="--proxy ${KEYSERVER_PROXY}" fi for keyserver in "${!keyservers_curl_map[@]}"; do local keyserver_curl_url="${keyservers_curl_map[${keyserver}]}" - if curl -s ${curl_args} --max-time 5 ${keyserver_curl_url} > /dev/null; then + if curl -s ${curl_args} --max-time 5 "${keyserver_curl_url}" > /dev/null; then echo "keyserver ${keyserver}" keyserver_reachable=true else @@ -100,31 +118,24 @@ get_gpg_key_servers() { fi } -# Import the specified key in a variable name passed in as receive_gpg_keys() { local keys=${!1} local keyring_args="" - if [ ! -z "$2" ]; then + if [ -n "${2:-}" ]; then keyring_args="--no-default-keyring --keyring \"$2\"" fi - # Install curl - if ! type curl > /dev/null 2>&1; then - check_packages curl - fi - - # Use a temporary location for gpg keys to avoid polluting image export GNUPGHOME="/tmp/tmp-gnupg" - mkdir -p ${GNUPGHOME} - chmod 700 ${GNUPGHOME} - echo -e "disable-ipv6\n$(get_gpg_key_servers)" > ${GNUPGHOME}/dirmngr.conf - # GPG key download sometimes fails for some reason and retrying fixes it. + mkdir -p "${GNUPGHOME}" + chmod 700 "${GNUPGHOME}" + echo -e "disable-ipv6\n$(get_gpg_key_servers)" > "${GNUPGHOME}/dirmngr.conf" + local retry_count=0 local gpg_ok="false" set +e - until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; - do + until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; do echo "(*) Downloading GPG key..." + # shellcheck disable=SC2086 ( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true" if [ "${gpg_ok}" != "true" ]; then echo "(*) Failed getting key, retrying in 10s..." @@ -139,306 +150,305 @@ receive_gpg_keys() { fi } -# Figure out correct version of a three part version number is not passed -find_version_from_git_tags() { - local variable_name=$1 - local requested_version=${!variable_name} - if [ "${requested_version}" = "none" ]; then return; fi - local repository=$2 - local prefix=${3:-"tags/v"} - local separator=${4:-"."} - local last_part_optional=${5:-"false"} - if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then - local escaped_separator=${separator//./\\.} - local last_part - if [ "${last_part_optional}" = "true" ]; then - last_part="(${escaped_separator}[0-9]+)?" - else - last_part="${escaped_separator}[0-9]+" - fi - local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" - local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" - if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then - declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" - else - set +e - declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" - set -e - fi - fi - if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then - echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 +default_ruby_version() { + [ -L "${RUBIES_DIR}/current" ] && basename "$(readlink "${RUBIES_DIR}/current")" +} + +install_build_deps() { + if command -v apt-get > /dev/null 2>&1; then + export DEBIAN_FRONTEND=noninteractive + apt-get update -y + # libgdbm-dev pulls the appropriate libgdbm runtime, so no version-specific package is needed. + apt-get -y install --no-install-recommends \ + curl ca-certificates git autoconf bison patch build-essential \ + libssl-dev libyaml-dev libreadline-dev zlib1g-dev libgmp-dev \ + libncurses-dev libffi-dev libgdbm-dev libdb-dev uuid-dev + elif command -v dnf > /dev/null 2>&1 || command -v yum > /dev/null 2>&1; then + local pm + pm="$(command -v dnf || command -v yum)" + "${pm}" install -y \ + curl ca-certificates git gcc make patch autoconf bison \ + openssl-devel libyaml-devel zlib-devel libffi-devel \ + readline-devel ncurses-devel gdbm-devel + elif command -v apk > /dev/null 2>&1; then + apk add --no-cache \ + bash curl ca-certificates git build-base linux-headers \ + autoconf bison patch openssl-dev yaml-dev zlib-dev \ + readline-dev ncurses-dev libffi-dev gdbm-dev + elif command -v zypper > /dev/null 2>&1; then + zypper --non-interactive install --no-recommends \ + curl ca-certificates git gcc-c++ make patch \ + autoconf automake libtool bison \ + libopenssl-devel libyaml-devel zlib-devel libffi-devel \ + readline-devel ncurses-devel gdbm-devel + elif command -v pacman > /dev/null 2>&1; then + pacman -Sy --noconfirm --needed \ + curl ca-certificates git base-devel autoconf bison \ + openssl libyaml zlib libffi readline ncurses gdbm + else + echo "(!) No supported package manager found. Install Ruby build dependencies manually." exit 1 fi - echo "${variable_name}=${!variable_name}" } -# Use semver logic to decrement a version number then look for the closest match -find_prev_version_from_git_tags() { - local variable_name=$1 - local current_version=${!variable_name} - local repository=$2 - # Normally a "v" is used before the version number, but support alternate cases - local prefix=${3:-"tags/v"} - # Some repositories use "_" instead of "." for version number part separation, support that - local separator=${4:-"."} - # Some tools release versions that omit the last digit (e.g. go) - local last_part_optional=${5:-"false"} - # Some repositories may have tags that include a suffix (e.g. actions/node-versions) - local version_suffix_regex=$6 - # Try one break fix version number less if we get a failure. Use "set +e" since "set -e" can cause failures in valid scenarios. - set +e - major="$(echo "${current_version}" | grep -oE '^[0-9]+' || echo '')" - minor="$(echo "${current_version}" | grep -oP '^[0-9]+\.\K[0-9]+' || echo '')" - breakfix="$(echo "${current_version}" | grep -oP '^[0-9]+\.[0-9]+\.\K[0-9]+' 2>/dev/null || echo '')" - - if [ "${minor}" = "0" ] && [ "${breakfix}" = "0" ]; then - ((major=major-1)) - declare -g ${variable_name}="${major}" - # Look for latest version from previous major release - find_version_from_git_tags "${variable_name}" "${repository}" "${prefix}" "${separator}" "${last_part_optional}" - # Handle situations like Go's odd version pattern where "0" releases omit the last part - elif [ "${breakfix}" = "" ] || [ "${breakfix}" = "0" ]; then - ((minor=minor-1)) - declare -g ${variable_name}="${major}.${minor}" - # Look for latest version from previous minor release - find_version_from_git_tags "${variable_name}" "${repository}" "${prefix}" "${separator}" "${last_part_optional}" - else - ((breakfix=breakfix-1)) - if [ "${breakfix}" = "0" ] && [ "${last_part_optional}" = "true" ]; then - declare -g ${variable_name}="${major}.${minor}" - else - declare -g ${variable_name}="${major}.${minor}.${breakfix}" - fi - fi - set -e +install_ruby_build() { + clone_or_update_repo "https://github.com/rbenv/ruby-build.git" "${RUBY_BUILD_DIR}" + ln -sf "${RUBY_BUILD_DIR}/bin/ruby-build" /usr/local/bin/ruby-build } -apt_get_update() -{ - if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then - echo "Running apt-get update..." - apt-get update -y +resolve_ruby_version() { + local requested=$1 + local definitions_dir="${RUBY_BUILD_DIR}/share/ruby-build" + local stable_versions + stable_versions="$(ls "${definitions_dir}" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V)" + + if [ -z "${stable_versions}" ]; then + echo "(!) ruby-build has no version definitions at ${definitions_dir}." >&2 + exit 1 fi + + case "${requested}" in + latest|current|lts) + echo "${stable_versions}" | tail -n 1 + ;; + *) + if echo "${stable_versions}" | grep -qx "${requested}"; then + echo "${requested}" + return + fi + # Resolve a partial X.Y to the highest matching X.Y.Z. + local match + match="$(echo "${stable_versions}" | grep -E "^${requested//./\\.}\\.[0-9]+$" | sort -V | tail -n 1)" + if [ -n "${match}" ]; then + echo "${match}" + return + fi + echo "(!) Ruby version '${requested}' is not known to ruby-build." >&2 + exit 1 + ;; + esac } -# Checks if packages are installed and installs them if not -check_packages() { - if ! dpkg -s "$@" > /dev/null 2>&1; then - apt_get_update - apt-get -y install --no-install-recommends "$@" +install_ruby_version() { + local requested=$1 + local set_default=$2 + local resolved + resolved="$(resolve_ruby_version "${requested}")" + local prefix="${RUBIES_DIR}/${resolved}" + + if [ -x "${prefix}/bin/ruby" ]; then + echo "(!) Ruby ${resolved} already installed at ${prefix}. Skipping..." + elif [ "${VERSION_MANAGER}" = "rbenv" ] && [ -x "${RBENV_ROOT}/bin/rbenv" ]; then + echo "Installing Ruby ${resolved} via rbenv..." + mkdir -p "${RUBIES_DIR}" + LANG="${LANG:-C.UTF-8}" RBENV_ROOT="${RBENV_ROOT}" \ + "${RBENV_ROOT}/bin/rbenv" install --skip-existing "${resolved}" + # Mirror into RUBIES_DIR so the rest of the script uses a consistent path. + ln -sfn "${RBENV_ROOT}/versions/${resolved}" "${prefix}" + elif [ "${VERSION_MANAGER}" = "rvm" ] && [ -s "${RVM_PATH}/scripts/rvm" ]; then + echo "Installing Ruby ${resolved} via rvm..." + mkdir -p "${RUBIES_DIR}" + # shellcheck disable=SC1091 + source "${RVM_PATH}/scripts/rvm" + LANG="${LANG:-C.UTF-8}" rvm install "${resolved}" + local rvm_ruby="${RVM_PATH}/rubies/ruby-${resolved}" + if [ -d "${rvm_ruby}" ]; then + ln -sfn "${rvm_ruby}" "${prefix}" + fi + else + mkdir -p "${RUBIES_DIR}" + echo "Installing Ruby ${resolved} via ruby-build..." + # Ensure a UTF-8 locale so that rdoc (and other tools bundled with Ruby) + # can process non-ASCII bytes during `make install`, even on minimal + # base images that ship with no LANG set (e.g. Debian 11 bullseye). + LANG="${LANG:-C.UTF-8}" ruby-build "${resolved}" "${prefix}" + fi + + if [ "${set_default}" = "true" ]; then + ln -sfn "${prefix}" "${RUBIES_DIR}/current" fi } -# Ensure apt is in non-interactive to avoid prompts -export DEBIAN_FRONTEND=noninteractive +# Called before ruby versions are installed so that install_ruby_version() +# can delegate to 'rbenv install'. +install_rbenv() { + echo "Installing rbenv..." + clone_or_update_repo "https://github.com/rbenv/rbenv.git" "${RBENV_ROOT}" -architecture="$(uname -m)" -if [ "${architecture}" != "amd64" ] && [ "${architecture}" != "x86_64" ] && [ "${architecture}" != "arm64" ] && [ "${architecture}" != "aarch64" ]; then - echo "(!) Architecture $architecture unsupported" - exit 1 -fi + ln -sf "${RBENV_ROOT}/bin/rbenv" /usr/local/bin/rbenv -# Install dependencies -# Removed software-properties-common package from here as it has been removed for debian trixie(13) -check_packages curl ca-certificates build-essential gnupg2 libreadline-dev \ - procps dirmngr gawk autoconf automake bison libffi-dev libgdbm-dev libncurses5-dev \ - libsqlite3-dev libtool libyaml-dev pkg-config sqlite3 zlib1g-dev libgmp-dev libssl-dev -if ! type git > /dev/null 2>&1; then - check_packages git -fi + # Wire the already-installed ruby-build as an rbenv plugin so that + # 'rbenv install' works out of the box. + mkdir -p "${RBENV_ROOT}/plugins" + if [ ! -e "${RBENV_ROOT}/plugins/ruby-build" ]; then + ln -sfn "${RUBY_BUILD_DIR}" "${RBENV_ROOT}/plugins/ruby-build" + fi + mkdir -p "${RBENV_ROOT}/versions" + echo "rbenv ready at ${RBENV_ROOT}." +} -# Conditionally install software-properties-common (skip on Debian Trixie) -if type apt-get >/dev/null 2>&1; then - if [ -f /etc/os-release ]; then - . /etc/os-release - if [ "${ID}" = "debian" ] && [ "${VERSION_CODENAME}" = "trixie" ]; then - echo "Skipping software-properties-common on Debian Trixie." - else - check_packages software-properties-common +# Called after ruby versions are installed. +finalize_rbenv() { + # When rubies were installed via ruby-build (not 'rbenv install'), symlink them + # into rbenv's versions directory so 'rbenv versions' shows them. + # Skip entries that already point into RBENV_ROOT to avoid circular symlinks. + for ruby_dir in "${RUBIES_DIR}"/[0-9]*/; do + [ -d "${ruby_dir}" ] || continue + local ver + ver="$(basename "${ruby_dir%/}")" + local real_target + real_target="$(readlink -f "${ruby_dir%/}" 2>/dev/null || true)" + if [ "${real_target}" = "${RBENV_ROOT}/versions/${ver}" ]; then + continue fi - else - # Fallback for apt-based systems without /etc/os-release - check_packages software-properties-common + ln -sfn "${ruby_dir%/}" "${RBENV_ROOT}/versions/${ver}" + done + + # Set the rbenv global version to match the ruby-build default. + local default_ver + default_ver="$(default_ruby_version)" + if [ -n "${default_ver}" ]; then + echo "${default_ver}" > "${RBENV_ROOT}/version" fi -fi -# Function to fetch the version released prior to the latest version -get_previous_version() { - local url=$1 - local repo_url=$2 - variable_name=$3 - prev_version=${!variable_name} - - output=$(curl -s "$repo_url"); - - #install jq - check_packages jq - - message=$(echo "$output" | jq -r '.message') - - if [[ $message == "API rate limit exceeded"* ]]; then - echo -e "\nAn attempt to find latest version using GitHub Api Failed... \nReason: ${message}" - echo -e "\nAttempting to find latest version using GitHub tags." - find_prev_version_from_git_tags prev_version "$url" "tags/v" "_" - declare -g ${variable_name}="${prev_version}" - else - echo -e "\nAttempting to find latest version using GitHub Api." - version=$(echo "$output" | jq -r '.tag_name' | tr '_' '.') - declare -g ${variable_name}="${version#v}" - fi - echo "${variable_name}=${!variable_name}" -} + apply_group_perms "${RBENV_ROOT}" -get_github_api_repo_url() { - local url=$1 - echo "${url/https:\/\/github.com/https:\/\/api.github.com\/repos}/releases/latest" + # Profile script for login shells (non-login shells rely on containerEnv + # which already prepends RBENV_ROOT/shims and RBENV_ROOT/bin). + cat > /etc/profile.d/rbenv.sh << 'RBENV_PROFILE' +export RBENV_ROOT=/usr/local/share/rbenv +export PATH="${RBENV_ROOT}/bin:${RBENV_ROOT}/shims:${PATH}" +eval "$(rbenv init - --no-rehash)" 2>/dev/null || true +RBENV_PROFILE + chmod +x /etc/profile.d/rbenv.sh + + RBENV_ROOT="${RBENV_ROOT}" "${RBENV_ROOT}/bin/rbenv" rehash 2>/dev/null || true + echo "rbenv configured." } +# Called before ruby versions are installed so that install_ruby_version() +# can delegate to 'rvm install'. +install_rvm() { + echo "Installing rvm..." -# Figure out correct version of a three part version number is not passed -RUBY_URL="https://github.com/ruby/ruby" -ORIGINAL_RUBY_VERSION=$RUBY_VERSION -find_version_from_git_tags RUBY_VERSION $RUBY_URL "tags/v" "_" + receive_gpg_keys RVM_GPG_KEYS -set_rvm_install_args() { - RUBY_VERSION=$1 - if [ "${RUBY_VERSION}" = "none" ]; then - RVM_INSTALL_ARGS="" - elif [[ "$(ruby -v)" = *"${RUBY_VERSION}"* ]]; then - echo "(!) Ruby is already installed with version ${RUBY_VERSION}. Skipping..." - RVM_INSTALL_ARGS="" - else - if [ "${RUBY_VERSION}" = "latest" ] || [ "${RUBY_VERSION}" = "current" ] || [ "${RUBY_VERSION}" = "lts" ]; then - RVM_INSTALL_ARGS="--ruby" - RUBY_VERSION="" - else - RVM_INSTALL_ARGS="--ruby=${RUBY_VERSION}" - fi - if [ "${INSTALL_RUBY_TOOLS}" = "true" ]; then - SKIP_GEM_INSTALL="true" - else - DEFAULT_GEMS="" - fi + curl -sSL https://get.rvm.io | bash -s stable --path "${RVM_PATH}" + + # rvm is a shell function, so we must source it before calling 'rvm' below. + # shellcheck disable=SC1091 + if [ -s "${RVM_PATH}/scripts/rvm" ]; then + source "${RVM_PATH}/scripts/rvm" fi + echo "rvm ready at ${RVM_PATH}." } -install_previous_version() { - if [[ $ORIGINAL_RUBY_VERSION == "latest" ]]; then - repo_url=$(get_github_api_repo_url "$RUBY_URL") - get_previous_version "${RUBY_URL}" "${repo_url}" RUBY_VERSION - set_rvm_install_args $RUBY_VERSION - curl -sSL https://get.rvm.io | bash -s stable --ignore-dotfiles ${RVM_INSTALL_ARGS} --with-default-gems="${DEFAULT_GEMS}" 2>&1 - else - echo "Failed to install Ruby version $ORIGINAL_RUBY_VERSION. Exiting..." +finalize_rvm() { + # shellcheck disable=SC1091 + [ -s "${RVM_PATH}/scripts/rvm" ] && source "${RVM_PATH}/scripts/rvm" || true + + # When rubies were installed via ruby-build (not 'rvm install'), mount them + # into rvm so 'rvm list' shows them. Skip entries that already live under + # RVM_PATH to avoid double-mounting. + if [ -d "${RUBIES_DIR}" ]; then + for ruby_dir in "${RUBIES_DIR}"/[0-9]*/; do + [ -d "${ruby_dir}" ] || continue + local ver + ver="$(basename "${ruby_dir%/}")" + local real_target + real_target="$(readlink -f "${ruby_dir%/}" 2>/dev/null || true)" + if [[ "${real_target}" == "${RVM_PATH}/rubies/"* ]]; then + continue + fi + rvm mount "${ruby_dir%/}" -n "${ver}" 2>/dev/null || true + done fi -} -# Just install Ruby if RVM already installed -if rvm --version > /dev/null; then - echo "Ruby Version Manager already exists." - if [[ "$(ruby -v)" = *"${RUBY_VERSION}"* ]]; then - echo "(!) Ruby is already installed with version ${RUBY_VERSION}. Skipping..." - elif [ "${RUBY_VERSION}" != "none" ]; then - echo "Installing specified Ruby version." - su ${USERNAME} -c "rvm install ruby ${RUBY_VERSION}" + # Set the rvm default to match the ruby-build default. + local default_ver + default_ver="$(default_ruby_version)" + if [ -n "${default_ver}" ]; then + local real_current + real_current="$(readlink -f "${RUBIES_DIR}/current" 2>/dev/null || true)" + if [[ "${real_current}" == "${RVM_PATH}/rubies/"* ]]; then + # Installed via 'rvm install': use the version name directly. + rvm use "${default_ver}" --default 2>/dev/null || true + else + # Installed via ruby-build and mounted: use the 'ext-' prefix. + rvm use "ext-${default_ver}" --default 2>/dev/null || true + fi fi - SKIP_GEM_INSTALL="false" - SKIP_RBENV_RBUILD="true" -else - # Install RVM - receive_gpg_keys RVM_GPG_KEYS - # Determine appropriate settings for rvm installer - set_rvm_install_args $RUBY_VERSION - # Create rvm group as a system group to reduce the odds of conflict with local user UIDs - if ! cat /etc/group | grep -e "^rvm:" > /dev/null 2>&1; then - groupadd -r rvm + + echo "source ${RVM_PATH}/scripts/rvm" > /etc/profile.d/rvm.sh + chmod +x /etc/profile.d/rvm.sh + + if [ "${USERNAME}" != "root" ] && id -u "${USERNAME}" > /dev/null 2>&1; then + usermod -aG rvm "${USERNAME}" 2>/dev/null || true fi - # Install rvm - curl -sSL https://get.rvm.io | bash -s stable --ignore-dotfiles ${RVM_INSTALL_ARGS} --with-default-gems="${DEFAULT_GEMS}" 2>&1 || install_previous_version - usermod -aG rvm ${USERNAME} - source /usr/local/rvm/scripts/rvm - rvm fix-permissions system - rm -rf ${GNUPGHOME} -fi + echo "rvm configured." +} + +install_build_deps +install_ruby_build -if [ "${INSTALL_RUBY_TOOLS}" = "true" ]; then - # Non-root user may not have "gem" in path when script is run and no ruby version - # is installed by rvm, so handle this by using root's default gem in this case - ROOT_GEM="$(which gem || echo "")" - ${ROOT_GEM} install ${DEFAULT_GEMS} +# Create a shared "ruby" group so the configured user can write under the rubies tree. +if ! getent group "${RUBY_GROUP}" > /dev/null 2>&1; then + groupadd -r "${RUBY_GROUP}" 2>/dev/null || addgroup -S "${RUBY_GROUP}" 2>/dev/null || true +fi +mkdir -p "${RUBIES_DIR}" +chgrp "${RUBY_GROUP}" "${RUBIES_DIR}" 2>/dev/null || true +chmod 2775 "${RUBIES_DIR}" 2>/dev/null || true + +# Set up the version manager BEFORE installing Ruby versions so that +# install_ruby_version() can delegate to it when requested. +if [ "${VERSION_MANAGER}" = "rbenv" ]; then + install_rbenv +elif [ "${VERSION_MANAGER}" = "rvm" ]; then + install_rvm fi -# VS Code server usually first in the path, so silence annoying rvm warning (that does not apply) and then source it -updaterc "if ! grep rvm_silence_path_mismatch_check_flag \$HOME/.rvmrc > /dev/null 2>&1; then echo 'rvm_silence_path_mismatch_check_flag=1' >> \$HOME/.rvmrc; fi\nsource /usr/local/rvm/scripts/rvm > /dev/null 2>&1" +if [ "${RUBY_VERSION}" != "none" ]; then + install_ruby_version "${RUBY_VERSION}" "true" +fi -# Additional ruby versions to be installed but not be set as default. if [ ! -z "${ADDITIONAL_VERSIONS}" ]; then OLDIFS=$IFS IFS="," read -a additional_versions <<< "$ADDITIONAL_VERSIONS" for version in "${additional_versions[@]}"; do - # Figure out correct version of a three part version number is not passed - find_version_from_git_tags version $RUBY_URL "tags/v" "_" - source /usr/local/rvm/scripts/rvm - rvm install ruby ${version} + install_ruby_version "${version}" "false" done IFS=$OLDIFS fi -# Install rbenv/ruby-build for good measure -if [ "${SKIP_RBENV_RBUILD}" != "true" ]; then +# Expose the default Ruby on the PATH for all login shells. +echo 'export PATH="/usr/local/rubies/current/bin:${PATH}"' > /etc/profile.d/ruby.sh +chmod +x /etc/profile.d/ruby.sh - if [[ ! -d "/usr/local/share/rbenv" ]]; then - git clone --depth=1 \ - -c core.eol=lf \ - -c core.autocrlf=false \ - -c fsck.zeroPaddedFilemode=ignore \ - -c fetch.fsck.zeroPaddedFilemode=ignore \ - -c receive.fsck.zeroPaddedFilemode=ignore \ - https://github.com/rbenv/rbenv.git /usr/local/share/rbenv - fi - - if [[ ! -d "/usr/local/share/ruby-build" ]]; then - git clone --depth=1 \ - -c core.eol=lf \ - -c core.autocrlf=false \ - -c fsck.zeroPaddedFilemode=ignore \ - -c fetch.fsck.zeroPaddedFilemode=ignore \ - -c receive.fsck.zeroPaddedFilemode=ignore \ - https://github.com/rbenv/ruby-build.git /usr/local/share/ruby-build - mkdir -p /root/.rbenv/plugins - - ln -s /usr/local/share/ruby-build /root/.rbenv/plugins/ruby-build - fi - - if [ "${USERNAME}" != "root" ]; then - mkdir -p /home/${USERNAME}/.rbenv/plugins - - if [[ ! -d "/home/${USERNAME}/.rbenv/plugins/ruby-build" ]]; then - ln -s /usr/local/share/ruby-build /home/${USERNAME}/.rbenv/plugins/ruby-build - fi - - # Oryx expects ruby to be installed in this specific path, else it breaks the oryx magic for ruby projects. - if [ ! -f /usr/local/rvm/gems/default/bin/ruby ]; then - ln -s /usr/local/rvm/rubies/default/bin/ruby /usr/local/rvm/gems/default/bin - fi +if [ "${RUBY_VERSION}" != "none" ] && [ "${INSTALL_RUBY_TOOLS}" = "true" ]; then + "${RUBIES_DIR}/current/bin/gem" install --no-document ${DEFAULT_GEMS} +fi - chown -R "${USERNAME}:rvm" "/home/${USERNAME}/.rbenv/" - chmod -R g+r+w "/home/${USERNAME}/.rbenv" - find "/home/${USERNAME}/.rbenv" -type d | xargs -n 1 chmod g+s +# Make sure the configured user can install gems against the shared rubies tree. +if [ "${USERNAME}" != "root" ] && id -u "${USERNAME}" > /dev/null 2>&1; then + if command -v usermod > /dev/null 2>&1; then + usermod -aG "${RUBY_GROUP}" "${USERNAME}" || true + elif command -v addgroup > /dev/null 2>&1; then + addgroup "${USERNAME}" "${RUBY_GROUP}" || true fi fi -chown -R "${USERNAME}:rvm" "/usr/local/rvm/" -chmod -R g+r+w "/usr/local/rvm/" -find "/usr/local/rvm/" -type d | xargs -n 1 chmod g+s +apply_group_perms "${RUBIES_DIR}" -# Clean up -rvm cleanup all -${ROOT_GEM} cleanup +if command -v apt-get > /dev/null 2>&1; then + rm -rf /var/lib/apt/lists/* +fi -# Clean up -rm -rf /var/lib/apt/lists/* +# Finalize the version manager now that all ruby versions are installed. +if [ "${VERSION_MANAGER}" = "rbenv" ]; then + finalize_rbenv +elif [ "${VERSION_MANAGER}" = "rvm" ]; then + finalize_rvm +fi echo "Done!" diff --git a/test/ruby/install_additional_ruby.sh b/test/ruby/install_additional_ruby.sh index 12b77def3..90c34f724 100644 --- a/test/ruby/install_additional_ruby.sh +++ b/test/ruby/install_additional_ruby.sh @@ -5,11 +5,11 @@ set -e # Optional: Import test library source dev-container-features-test-lib -check "ruby version 3.4.2 installed as default" ruby -v | grep 3.4.2 -check "ruby version 3.2.8 installed" rvm list | grep 3.2.8 -check "ruby version 3.3.2 installed" rvm list | grep 3.3.2 - -check "rbenv" bash -c 'eval "$(rbenv init -)" && rbenv --version' +check "ruby-build available" ruby-build --version +check "ruby version 3.4.2 installed as default" bash -c "ruby -v | grep 3.4.2" +check "ruby version 3.4.2 prefix present" test -x /usr/local/rubies/3.4.2/bin/ruby +check "ruby version 3.3.2 prefix present" test -x /usr/local/rubies/3.3.2/bin/ruby +check "ruby version 3.2 series installed" bash -c "ls /usr/local/rubies | grep -E '^3\\.2\\.'" check "rake" bash -c "gem list | grep rake" # Report result diff --git a/test/ruby/install_additional_ruby_trixie.sh b/test/ruby/install_additional_ruby_trixie.sh deleted file mode 100644 index 76f7c9028..000000000 --- a/test/ruby/install_additional_ruby_trixie.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -set -e - -# Optional: Import test library -source dev-container-features-test-lib - -check "ruby version 3.4.2 installed as default" ruby -v | grep 3.4.2 -check "ruby version 3.2.8 installed" rvm list | grep 3.2.8 -check "ruby version 3.3.2 installed" rvm list | grep 3.3.2 - -check "rbenv" bash -c 'eval "$(rbenv init -)" && rbenv --version' -check "rake" bash -c "gem list | grep rake" - -# Report result -reportResults - diff --git a/test/ruby/install_additional_ruby_trixie.sh b/test/ruby/install_additional_ruby_trixie.sh new file mode 120000 index 000000000..bddda2d3a --- /dev/null +++ b/test/ruby/install_additional_ruby_trixie.sh @@ -0,0 +1 @@ +install_additional_ruby.sh \ No newline at end of file diff --git a/test/ruby/install_ruby_trixie_base.sh b/test/ruby/install_ruby_trixie_base.sh index f90c76cc4..609187ccb 100644 --- a/test/ruby/install_ruby_trixie_base.sh +++ b/test/ruby/install_ruby_trixie_base.sh @@ -5,11 +5,11 @@ set -e # Optional: Import test library source dev-container-features-test-lib -# Definition specific tests -check "ruby version" ruby --version -check "rvm" rvm --version +# The feature was invoked with version=none on a base image that already ships +# Ruby. ruby-build should still be installed so additional versions can be added. +check "ruby version" ruby --version check "gem version" gem --version +check "ruby-build available" ruby-build --version # Report result reportResults - diff --git a/test/ruby/ruby_fallback_test.sh b/test/ruby/ruby_fallback_test.sh deleted file mode 100644 index a4ec9a6b5..000000000 --- a/test/ruby/ruby_fallback_test.sh +++ /dev/null @@ -1,310 +0,0 @@ -#!/bin/bash - -set -e - -# Optional: Import test library -source dev-container-features-test-lib - -USERNAME="automatic" -echo -e "\nRVM version installed previously by ruby feature ..." -check "rvm" rvm --version -check "ruby" ruby -v - -trap 'echo "Last executed command failed at line ${LINENO}"' ERR - -RVM_GPG_KEYS="409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB" - -# Clean up -rm -rf /var/lib/apt/lists/* - -# Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi - -# Ensure apt is in non-interactive to avoid prompts -export DEBIAN_FRONTEND=noninteractive - -architecture="$(uname -m)" -if [ "${architecture}" != "amd64" ] && [ "${architecture}" != "x86_64" ] && [ "${architecture}" != "arm64" ] && [ "${architecture}" != "aarch64" ]; then - echo "(!) Architecture $architecture unsupported" - exit 1 -fi - -apt_get_update() -{ - if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then - echo "Running apt-get update..." - apt-get update -y - fi -} - -# Checks if packages are installed and installs them if not -check_packages() { - if ! dpkg -s "$@" > /dev/null 2>&1; then - apt_get_update - apt-get -y install --no-install-recommends "$@" - fi -} - -# Get the list of GPG key servers that are reachable -get_gpg_key_servers() { - declare -A keyservers_curl_map=( - ["hkp://keyserver.ubuntu.com"]="http://keyserver.ubuntu.com:11371" - ["hkp://keyserver.ubuntu.com:80"]="http://keyserver.ubuntu.com" - ["hkps://keys.openpgp.org"]="https://keys.openpgp.org" - ["hkp://keyserver.pgp.com"]="http://keyserver.pgp.com:11371" - ) - - local curl_args="" - local keyserver_reachable=false # Flag to indicate if any keyserver is reachable - - if [ ! -z "${KEYSERVER_PROXY}" ]; then - curl_args="--proxy ${KEYSERVER_PROXY}" - fi - - for keyserver in "${!keyservers_curl_map[@]}"; do - local keyserver_curl_url="${keyservers_curl_map[${keyserver}]}" - if curl -s ${curl_args} --max-time 5 ${keyserver_curl_url} > /dev/null; then - echo "keyserver ${keyserver}" - keyserver_reachable=true - else - echo "(*) Keyserver ${keyserver} is not reachable." >&2 - fi - done - - if ! $keyserver_reachable; then - echo "(!) No keyserver is reachable." >&2 - exit 1 - fi -} - -# Import the specified key in a variable name passed in as -receive_gpg_keys() { - local keys=${!1} - local keyring_args="" - if [ ! -z "$2" ]; then - keyring_args="--no-default-keyring --keyring \"$2\"" - fi - - # Install curl - if ! type curl > /dev/null 2>&1; then - check_packages curl - fi - - # Use a temporary location for gpg keys to avoid polluting image - export GNUPGHOME="/tmp/tmp-gnupg" - mkdir -p ${GNUPGHOME} - chmod 700 ${GNUPGHOME} - echo -e "disable-ipv6\n$(get_gpg_key_servers)" | tee ${GNUPGHOME}/dirmngr.conf > /dev/null - # GPG key download sometimes fails for some reason and retrying fixes it. - local retry_count=0 - local gpg_ok="false" - set +e - until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; - do - echo "(*) Downloading GPG key..." - ( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true" - if [ "${gpg_ok}" != "true" ]; then - echo "(*) Failed getting key, retrying in 10s..." - (( retry_count++ )) - sleep 10s - fi - done - set -e - if [ "${gpg_ok}" = "false" ]; then - echo "(!) Failed to get gpg key." - exit 1 - fi -} - -# Figure out correct version of a three part version number is not passed -find_version_from_git_tags() { - local variable_name=$1 - local requested_version=${!variable_name} - if [ "${requested_version}" = "none" ]; then return; fi - local repository=$2 - local prefix=${3:-"tags/v"} - local separator=${4:-"."} - local last_part_optional=${5:-"false"} - if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then - local escaped_separator=${separator//./\\.} - local last_part - if [ "${last_part_optional}" = "true" ]; then - last_part="(${escaped_separator}[0-9]+)?" - else - last_part="${escaped_separator}[0-9]+" - fi - local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" - local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" - if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then - declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" - else - set +e - declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" - set -e - fi - fi - if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then - echo -e "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 - exit 1 - fi - echo "${variable_name}=${!variable_name}" -} - -# Use semver logic to decrement a version number then look for the closest match -find_prev_version_from_git_tags() { - local variable_name=$1 - local current_version=${!variable_name} - local repository=$2 - # Normally a "v" is used before the version number, but support alternate cases - local prefix=${3:-"tags/v"} - # Some repositories use "_" instead of "." for version number part separation, support that - local separator=${4:-"."} - # Some tools release versions that omit the last digit (e.g. go) - local last_part_optional=${5:-"false"} - # Some repositories may have tags that include a suffix (e.g. actions/node-versions) - local version_suffix_regex=$6 - # Try one break fix version number less if we get a failure. Use "set +e" since "set -e" can cause failures in valid scenarios. - set +e - major="$(echo "${current_version}" | grep -oE '^[0-9]+' || echo '')" - minor="$(echo "${current_version}" | grep -oP '^[0-9]+\.\K[0-9]+' || echo '')" - breakfix="$(echo "${current_version}" | grep -oP '^[0-9]+\.[0-9]+\.\K[0-9]+' 2>/dev/null || echo '')" - - if [ "${minor}" = "0" ] && [ "${breakfix}" = "0" ]; then - ((major=major-1)) - declare -g ${variable_name}="${major}" - # Look for latest version from previous major release - find_version_from_git_tags "${variable_name}" "${repository}" "${prefix}" "${separator}" "${last_part_optional}" - # Handle situations like Go's odd version pattern where "0" releases omit the last part - elif [ "${breakfix}" = "" ] || [ "${breakfix}" = "0" ]; then - ((minor=minor-1)) - declare -g ${variable_name}="${major}.${minor}" - # Look for latest version from previous minor release - find_version_from_git_tags "${variable_name}" "${repository}" "${prefix}" "${separator}" "${last_part_optional}" - else - ((breakfix=breakfix-1)) - if [ "${breakfix}" = "0" ] && [ "${last_part_optional}" = "true" ]; then - declare -g ${variable_name}="${major}.${minor}" - else - declare -g ${variable_name}="${major}.${minor}.${breakfix}" - fi - fi - set -e -} - -# Function to fetch the version released prior to the latest version -get_previous_version() { - local url=$1 - local repo_url=$2 - local variable_name=$3 - local mode=$4 - prev_version=${!variable_name} - - output=$(curl -s "$repo_url"); - - #install jq - check_packages jq - - message=$(echo "$output" | jq -r '.message') - - if [[ $mode == "mode1" ]]; then - message="API rate limit exceeded" - else - message="" - fi - - if [[ $message == "API rate limit exceeded"* ]]; then - echo -e "\nAn attempt to find latest version using GitHub Api Failed... \nReason: ${message}" - echo -e "\nAttempting to find latest version using GitHub tags." - find_prev_version_from_git_tags prev_version "$url" "tags/v" "_" - declare -g ${variable_name}="${prev_version}" - else - echo -e "\nAttempting to find latest version using GitHub Api." - version=$(echo "$output" | jq -r '.tag_name' | tr '_' '.') - declare -g ${variable_name}="${version#v}" - fi - echo "${variable_name}=${!variable_name}" -} - -get_github_api_repo_url() { - local url=$1 - echo "${url/https:\/\/github.com/https:\/\/api.github.com\/repos}/releases/latest" -} - - -# Figure out correct version of a three part version number is not passed -ruby_url="https://github.com/ruby/ruby" - -RUBY_VERSION="3.4.xyz" - -set_rvm_install_args() { - RUBY_VERSION=$1 - if [ "${RUBY_VERSION}" = "none" ]; then - RVM_INSTALL_ARGS="" - elif [[ "$(ruby -v)" = *"${RUBY_VERSION}"* ]]; then - echo "(!) Ruby is already installed with version ${RUBY_VERSION}. Skipping..." - RVM_INSTALL_ARGS="" - else - if [ "${RUBY_VERSION}" = "latest" ] || [ "${RUBY_VERSION}" = "current" ] || [ "${RUBY_VERSION}" = "lts" ]; then - RVM_INSTALL_ARGS="--ruby" - RUBY_VERSION="" - else - RVM_INSTALL_ARGS="--ruby=${RUBY_VERSION}" - fi - if [ "${INSTALL_RUBY_TOOLS}" = "true" ]; then - SKIP_GEM_INSTALL="true" - else - DEFAULT_GEMS="" - fi - fi -} - -install_previous_version() { - mode=$1 - repo_url=$(get_github_api_repo_url "$ruby_url") - get_previous_version "${ruby_url}" "${repo_url}" RUBY_VERSION $mode - set_rvm_install_args $RUBY_VERSION - curl -sSL https://get.rvm.io | bash -s stable --ignore-dotfiles ${RVM_INSTALL_ARGS} --with-default-gems="${DEFAULT_GEMS}" 2>&1 -} - -install_rvm() { - mode=$1 - # Install RVM - receive_gpg_keys RVM_GPG_KEYS - # Determine appropriate settings for rvm installer - set_rvm_install_args $RUBY_VERSION - # Create rvm group as a system group to reduce the odds of conflict with local user UIDs - if ! cat /etc/group | grep -e "^rvm:" > /dev/null 2>&1; then - groupadd -r rvm - fi - # Install rvm - curl -sSL https://get.rvm.io | bash -s stable --ignore-dotfiles ${RVM_INSTALL_ARGS} --with-default-gems="${DEFAULT_GEMS}" 2>&1 || install_previous_version "$mode" - sudo usermod -aG rvm ${USERNAME} - source /usr/local/rvm/scripts/rvm - rvm fix-permissions system - rm -rf ${GNUPGHOME} -} - -install_rvm "mode1" -echo -e "\n๐Ÿ‘‰๐Ÿป๐Ÿ‘‰๐ŸปRVM version installed by test file ... (mode: 1 - install using find_prev_version_from_git_tags):" -check "rvm" rvm --version - -install_rvm "mode2" -echo -e "\n๐Ÿ‘‰๐Ÿป๐Ÿ‘‰๐ŸปRVM version installed by test file ... (mode: 1 - install using GitHub Api):" -check "rvm" rvm --version - -# Report result -reportResults \ No newline at end of file diff --git a/test/ruby/ruby_rbenv.sh b/test/ruby/ruby_rbenv.sh new file mode 100755 index 000000000..685dc6828 --- /dev/null +++ b/test/ruby/ruby_rbenv.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# rbenv (and its shims/bin dirs) is placed on PATH via containerEnv, +# so these commands should work in non-login shells too. +check "ruby version 3.4.2 active" bash -c "ruby -v | grep 3.4.2" +check "rbenv available" rbenv --version +check "rbenv lists ruby 3.4.2" bash -c "rbenv versions | grep 3.4.2" +check "rbenv global is 3.4.2" bash -c "rbenv global | grep 3.4.2" +check "rbenv shim for ruby" test -x /usr/local/share/rbenv/shims/ruby +check "ruby-build wired as rbenv plugin" test -d /usr/local/share/rbenv/plugins/ruby-build +check "rake gem installed" bash -c "gem list | grep rake" + +# Report result +reportResults diff --git a/test/ruby/ruby_rvm.sh b/test/ruby/ruby_rvm.sh new file mode 100755 index 000000000..6cea7901a --- /dev/null +++ b/test/ruby/ruby_rvm.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Ruby installed via rvm lives under /usr/local/rvm/rubies and is also +# exposed via the /usr/local/rubies/current PATH entry from containerEnv. +check "ruby version 3.4.2 active" bash -c "ruby -v | grep 3.4.2" +check "rvm binary available" /usr/local/rvm/bin/rvm --version +check "rvm ruby 3.4.2 directory exists" test -d /usr/local/rvm/rubies/ruby-3.4.2 +# rvm is implemented as a shell function, so source it before calling. +check "rvm default points to 3.4.2" bash -c "source /usr/local/rvm/scripts/rvm && rvm current | grep 3.4.2" +check "rvm profile.d hook installed" test -x /etc/profile.d/rvm.sh +check "rake gem installed" bash -c "gem list | grep rake" + +# Report result +reportResults diff --git a/test/ruby/scenarios.json b/test/ruby/scenarios.json index 7aa7c5f8e..4c3b2600c 100644 --- a/test/ruby/scenarios.json +++ b/test/ruby/scenarios.json @@ -1,8 +1,8 @@ -{ +{ "install_ruby_trixie_base": { "build": { "dockerfile": "Dockerfile" - }, + }, "features": { "ghcr.io/devcontainers/features/common-utils:2": { "installZsh": "true", @@ -28,7 +28,7 @@ "additionalVersions": "3.2,3.3.2" } } - }, + }, "install_additional_ruby": { "image": "ubuntu:noble", "features": { @@ -39,17 +39,27 @@ } }, "ruby_debian": { - "image": "mcr.microsoft.com/devcontainers/base:bullseye", + "image": "mcr.microsoft.com/devcontainers/base:bookworm", "features": { "ruby": {} } }, - "ruby_fallback_test": { - "image": "mcr.microsoft.com/devcontainers/base:bullseye", + "ruby_rbenv": { + "image": "mcr.microsoft.com/devcontainers/base:noble", "features": { "ruby": { - "version": "latest" + "version": "3.4.2", + "versionManager": "rbenv" + } + } + }, + "ruby_rvm": { + "image": "mcr.microsoft.com/devcontainers/base:bookworm", + "features": { + "ruby": { + "version": "3.4.2", + "versionManager": "rvm" } } } -} \ No newline at end of file +} From e21f5c8de89f9b0c191e39084a90d7dbb25ca8d0 Mon Sep 17 00:00:00 2001 From: Kaniska Date: Thu, 25 Jun 2026 12:59:47 +0530 Subject: [PATCH 04/21] [docker-in-docker] - Move the iptables switching logic in the docker-init script and isolated tests for specific cases (#1666) * Check the tests * check the log * Adding debug statements * Another change * Check in docker-init.sh * Change test order * Check the presence of the kernel module * change the test execution order * Changes in workflows * Change the test * Further isolation * Adding a flag to switch the logic for better management. * Modify stress tests * Add docker-compose latest version * Changing to minor version upgrade as a switch flag is present * Revert "Add docker-compose latest version" This reverts commit 7360873729d711c85bc3932b8a854ffd0f6a9e91. * Reapply "Add docker-compose latest version" This reverts commit dfa45209737192e4f4a5ca8e58222c8ce47c823b. * Removing docker-compose latest version change from this PR. * Implementing review comments. * Implementing review comments * Implementing review comments further * Setting iptablesSwitchAtRuntime:true and major version bump. --- ...r-in-docker-daemon-startup-bulk-test.yaml} | 6 +- .github/workflows/test-pr-arm64.yaml | 9 +++ .github/workflows/test-pr.yaml | 37 ++++++++++++ src/docker-in-docker/README.md | 3 +- .../devcontainer-feature.json | 7 ++- src/docker-in-docker/install.sh | 35 ++++++++++- .../docker_iptables_switch_at_install.sh | 22 +++++++ .../docker_iptables_switch_at_runtime.sh | 24 ++++++++ .../docker_with_default_iptables.sh | 33 +++++++++++ .../docker_with_default_iptables_ubuntu.sh | 1 + .../docker_with_legacy_iptables.sh | 20 +++++++ .../docker_with_legacy_iptables_ubuntu.sh | 1 + test/docker-in-docker/scenarios.json | 59 ++++++++++++++++++- 13 files changed, 248 insertions(+), 9 deletions(-) rename .github/workflows/{docker-in-docker-stress-test.yaml => docker-in-docker-daemon-startup-bulk-test.yaml} (82%) create mode 100644 test/docker-in-docker/docker_iptables_switch_at_install.sh create mode 100644 test/docker-in-docker/docker_iptables_switch_at_runtime.sh create mode 100644 test/docker-in-docker/docker_with_default_iptables.sh create mode 120000 test/docker-in-docker/docker_with_default_iptables_ubuntu.sh create mode 100644 test/docker-in-docker/docker_with_legacy_iptables.sh create mode 120000 test/docker-in-docker/docker_with_legacy_iptables_ubuntu.sh diff --git a/.github/workflows/docker-in-docker-stress-test.yaml b/.github/workflows/docker-in-docker-daemon-startup-bulk-test.yaml similarity index 82% rename from .github/workflows/docker-in-docker-stress-test.yaml rename to .github/workflows/docker-in-docker-daemon-startup-bulk-test.yaml index a63225a13..b7ec4339e 100644 --- a/.github/workflows/docker-in-docker-stress-test.yaml +++ b/.github/workflows/docker-in-docker-daemon-startup-bulk-test.yaml @@ -1,4 +1,4 @@ -name: "Stress test - Docker in Docker" +name: "Test Docker daemon startup in bulk - Docker in Docker" on: pull_request: paths: @@ -18,8 +18,8 @@ jobs: - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli - - name: "Generating tests for 'docker-in-docker' which validates if docker daemon is running" - run: devcontainer features test --skip-scenarios -f docker-in-docker -i mcr.microsoft.com/devcontainers/base:noble . + - name: "Generating tests for 'docker-in-docker' which validates if docker daemon is running (with iptablesSwitchAtRuntime=true)" + run: devcontainer features test -f docker-in-docker --skip-autogenerated --filter "docker_iptables_switch_at_runtime" . test-onCreate: strategy: diff --git a/.github/workflows/test-pr-arm64.yaml b/.github/workflows/test-pr-arm64.yaml index e5855ced2..ddc761000 100644 --- a/.github/workflows/test-pr-arm64.yaml +++ b/.github/workflows/test-pr-arm64.yaml @@ -75,5 +75,14 @@ jobs: - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli + - name: "Exclude iptables-isolation scenarios from docker-in-docker" + if: matrix.features == 'docker-in-docker' + run: | + sudo apt-get update && sudo apt-get install -y jq + sed 's://.*$::' test/docker-in-docker/scenarios.json \ + | jq 'del(.docker_with_default_iptables, .docker_with_default_iptables_ubuntu)' \ + > test/docker-in-docker/scenarios.json.tmp + mv test/docker-in-docker/scenarios.json.tmp test/docker-in-docker/scenarios.json + - name: "Testing '${{ matrix.features }}' scenarios" run: devcontainer features test -f ${{ matrix.features }} --skip-autogenerated . diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index e00c50876..8b2520752 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -92,5 +92,42 @@ jobs: - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli + - name: "Exclude iptables-isolation scenarios from docker-in-docker (run in separate 'iptables-isolation' job)" + if: matrix.features == 'docker-in-docker' + run: | + sudo apt-get update && sudo apt-get install -y jq + sed 's://.*$::' test/docker-in-docker/scenarios.json \ + | jq 'del(.docker_with_default_iptables, .docker_with_default_iptables_ubuntu)' \ + > test/docker-in-docker/scenarios.json.tmp + mv test/docker-in-docker/scenarios.json.tmp test/docker-in-docker/scenarios.json + - name: "Testing '${{ matrix.features }}' scenarios" run: devcontainer features test -f ${{ matrix.features }} --skip-autogenerated . + + iptables-isolation: + needs: [detect-changes] + if: contains(fromJSON(needs.detect-changes.outputs.features), 'docker-in-docker') + runs-on: ubuntu-latest + continue-on-error: true + strategy: + fail-fast: false + matrix: + scenario: + - docker_with_default_iptables + - docker_with_default_iptables_ubuntu + steps: + - uses: actions/checkout@v6 + + - name: "Install latest devcontainer CLI" + run: npm install -g @devcontainers/cli + + - name: "Isolate scenario '${{ matrix.scenario }}'" + run: | + sudo apt-get update && sudo apt-get install -y jq + sed 's://.*$::' test/docker-in-docker/scenarios.json \ + | jq '{ "${{ matrix.scenario }}": .["${{ matrix.scenario }}"] }' \ + > test/docker-in-docker/scenarios.json.tmp + mv test/docker-in-docker/scenarios.json.tmp test/docker-in-docker/scenarios.json + + - name: "Testing docker-in-docker scenario '${{ matrix.scenario }}'" + run: devcontainer features test --features docker-in-docker --filter ${{ matrix.scenario }} --skip-autogenerated . diff --git a/src/docker-in-docker/README.md b/src/docker-in-docker/README.md index f94c9be51..6bba801a8 100644 --- a/src/docker-in-docker/README.md +++ b/src/docker-in-docker/README.md @@ -7,7 +7,7 @@ Create child containers *inside* a container, independent from the host's docker ```json "features": { - "ghcr.io/devcontainers/features/docker-in-docker:3": {} + "ghcr.io/devcontainers/features/docker-in-docker:4": {} } ``` @@ -24,6 +24,7 @@ Create child containers *inside* a container, independent from the host's docker | installDockerBuildx | Install Docker Buildx | boolean | true | | installDockerComposeSwitch | Install Compose Switch (provided docker compose is available) which is a replacement to the Compose V1 docker-compose (python) executable. It translates the command line into Compose V2 docker compose then runs the latter. | boolean | false | | disableIp6tables | Disable ip6tables (this option is only applicable for Docker versions 27 and greater) | boolean | false | +| iptablesSwitchAtRuntime | If true, the iptables alternative is selected at container start (inside docker-init.sh) instead of at image build time. Useful when the desired iptables backend depends on the host kernel at runtime rather than at build time. | boolean | true | ## Customizations diff --git a/src/docker-in-docker/devcontainer-feature.json b/src/docker-in-docker/devcontainer-feature.json index 6d7c0431a..2710a2e32 100644 --- a/src/docker-in-docker/devcontainer-feature.json +++ b/src/docker-in-docker/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "docker-in-docker", - "version": "3.1.0", + "version": "4.0.0", "name": "Docker (Docker-in-Docker)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/docker-in-docker", "description": "Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.", @@ -61,6 +61,11 @@ "type": "boolean", "default": false, "description": "Disable ip6tables (this option is only applicable for Docker versions 27 and greater)" + }, + "iptablesSwitchAtRuntime": { + "type": "boolean", + "default": true, + "description": "If true, the iptables alternative is selected at container start (inside docker-init.sh) instead of at image build time. Useful when the desired iptables backend depends on the host kernel at runtime rather than at build time." } }, "entrypoint": "/usr/local/share/docker-init.sh", diff --git a/src/docker-in-docker/install.sh b/src/docker-in-docker/install.sh index 70a187e28..dbef9ae32 100755 --- a/src/docker-in-docker/install.sh +++ b/src/docker-in-docker/install.sh @@ -22,6 +22,7 @@ MICROSOFT_GPG_KEYS_ROLLING_URI="https://packages.microsoft.com/keys/microsoft-ro DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES="trixie bookworm buster bullseye bionic focal jammy noble" DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES="trixie bookworm buster bullseye bionic focal hirsute impish jammy noble resolute" DISABLE_IP6_TABLES="${DISABLEIP6TABLES:-false}" +IPTABLES_SWITCH_AT_RUNTIME="${IPTABLESSWITCHATRUNTIME:-true}" # Default: Exit on any failure. set -e @@ -313,8 +314,10 @@ if [ "${ADJUSTED_ID}" = "debian" ] && command -v update-ca-certificates > /dev/n update-ca-certificates fi -# Swap to legacy iptables for compatibility (Debian only) -if [ "${ADJUSTED_ID}" = "debian" ]; then +# Swap to legacy iptables for compatibility (Debian only) - install-time path. +# When IPTABLES_SWITCH_AT_RUNTIME=true the same logic is emitted into +# docker-init.sh and runs at container start instead. +if [ "${IPTABLES_SWITCH_AT_RUNTIME}" != "true" ] && [ "${ADJUSTED_ID}" = "debian" ]; then # On distros where legacy iptables is no longer kernel-supported (e.g. Ubuntu 26.04 / resolute), # prefer iptables-nft. Otherwise prefer legacy for backward compatibility. use_nft=false @@ -323,12 +326,15 @@ if [ "${ADJUSTED_ID}" = "debian" ]; then esac if [ "${use_nft}" = "true" ] && type iptables-nft > /dev/null 2>&1; then + echo "(*) Setting iptables alternatives to nft for better compatibility with newer kernels" update-alternatives --set iptables /usr/sbin/iptables-nft || true update-alternatives --set ip6tables /usr/sbin/ip6tables-nft || true - elif type iptables-legacy > /dev/null 2>&1; then + elif type iptables-legacy > /dev/null 2>&1 && iptables-legacy -L > /dev/null 2>&1; then + echo "(*) Setting iptables alternatives to legacy for better compatibility with Docker and older kernels" update-alternatives --set iptables /usr/sbin/iptables-legacy || true update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy || true elif type iptables-nft > /dev/null 2>&1; then + echo "(*) Setting iptables alternatives to nft for better compatibility with newer kernels for non resolute" update-alternatives --set iptables /usr/sbin/iptables-nft || true update-alternatives --set ip6tables /usr/sbin/ip6tables-nft || true fi @@ -970,6 +976,29 @@ DOCKER_DEFAULT_ADDRESS_POOL=${DOCKER_DEFAULT_ADDRESS_POOL} DOCKER_DEFAULT_IP6_TABLES=${DOCKER_DEFAULT_IP6_TABLES} EOF +# On Debian-based images, re-assert the iptables alternative at container start +# (only when the user opted into runtime switching via iptablesSwitchAtRuntime=true). +if [ "${IPTABLES_SWITCH_AT_RUNTIME}" = "true" ] && [ "${ADJUSTED_ID}" = "debian" ]; then + tee -a /usr/local/share/docker-init.sh > /dev/null \ +<< 'EOF' +# Prefer legacy only when the ip_tables kernel module is actually present. +# (Do NOT call `iptables-legacy -L/-nL` to test this โ€” it auto-modprobes ip_tables +# and would defeat hosts/scenarios where the module is intentionally absent +# such as the newer kernels which leaves out ip_tables legacy.) +if type iptables-legacy > /dev/null 2>&1 \ + && { grep -qE '^(ip_tables)\b' /proc/modules \ + || [ -d /sys/module/ip_tables ]; } \ + && update-alternatives --list iptables 2>/dev/null | grep -q '/usr/sbin/iptables-legacy'; then + update-alternatives --set iptables /usr/sbin/iptables-legacy || true + update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy || true +elif type iptables-nft > /dev/null 2>&1 \ + && update-alternatives --list iptables 2>/dev/null | grep -q '/usr/sbin/iptables-nft'; then + update-alternatives --set iptables /usr/sbin/iptables-nft || true + update-alternatives --set ip6tables /usr/sbin/ip6tables-nft || true +fi +EOF +fi + tee -a /usr/local/share/docker-init.sh > /dev/null \ << 'EOF' dockerd_start="AZURE_DNS_AUTO_DETECTION=${AZURE_DNS_AUTO_DETECTION} DOCKER_DEFAULT_ADDRESS_POOL=${DOCKER_DEFAULT_ADDRESS_POOL} DOCKER_DEFAULT_IP6_TABLES=${DOCKER_DEFAULT_IP6_TABLES} $(cat << 'INNEREOF' diff --git a/test/docker-in-docker/docker_iptables_switch_at_install.sh b/test/docker-in-docker/docker_iptables_switch_at_install.sh new file mode 100644 index 000000000..c87650a4c --- /dev/null +++ b/test/docker-in-docker/docker_iptables_switch_at_install.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Default behavior (iptablesSwitchAtRuntime omitted -> false): switching happens +# at image build time, so docker-init.sh should NOT contain the runtime block. +check "init-script-exists" bash -c "test -f /usr/local/share/docker-init.sh" +check "no-runtime-iptables-block" bash -c "! grep -q 'update-alternatives --set iptables' /usr/local/share/docker-init.sh" + +# The build-time switch should have set /etc/alternatives/iptables to one of the +# known backends. With the ip_tables module loaded on the host, legacy is preferred. +check "iptables-alternative-set" bash -c "readlink /etc/alternatives/iptables | grep -E 'iptables-(legacy|nft)$'" +check "iptables works" sudo iptables -L + +check "version" docker --version +check "docker-ps" bash -c "docker ps" + +# Report result +reportResults diff --git a/test/docker-in-docker/docker_iptables_switch_at_runtime.sh b/test/docker-in-docker/docker_iptables_switch_at_runtime.sh new file mode 100644 index 000000000..152a4ec98 --- /dev/null +++ b/test/docker-in-docker/docker_iptables_switch_at_runtime.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# iptablesSwitchAtRuntime=true: switching is deferred to container start, so the +# runtime block MUST have been written into docker-init.sh by install.sh. +check "init-script-exists" bash -c "test -f /usr/local/share/docker-init.sh" +check "runtime-iptables-block-present" bash -c "grep -q 'update-alternatives --set iptables' /usr/local/share/docker-init.sh" +check "runtime-iptables-block-has-legacy-branch" bash -c "grep -q '/usr/sbin/iptables-legacy' /usr/local/share/docker-init.sh" +check "runtime-iptables-block-has-nft-branch" bash -c "grep -q '/usr/sbin/iptables-nft' /usr/local/share/docker-init.sh" + +# The runtime block runs as part of docker-init.sh (the feature's entrypoint), +# so by the time these tests execute the alternative must already be set. +check "iptables-alternative-set" bash -c "readlink /etc/alternatives/iptables | grep -E 'iptables-(legacy|nft)$'" +check "iptables works" sudo iptables -L + +check "version" docker --version +check "docker-ps" bash -c "docker ps" + +# Report result +reportResults diff --git a/test/docker-in-docker/docker_with_default_iptables.sh b/test/docker-in-docker/docker_with_default_iptables.sh new file mode 100644 index 000000000..8336cda4c --- /dev/null +++ b/test/docker-in-docker/docker_with_default_iptables.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Feature specific tests +check "docker-ps" bash -c "docker ps" +# Fail loudly if dockerd never finished initializing, printing the real error +check "dockerd-started-successfully" bash -c ' + if ! grep -q "Daemon has completed initialization" /tmp/dockerd.log; then + echo "โŒ Docker daemon failed to start. Last errors from /tmp/dockerd.log:" + echo "----- dockerd.log (tail) -----" + tail -n 100 /tmp/dockerd.log + echo "----- error/fatal lines -----" + grep -iE "error|fatal|failed|panic" /tmp/dockerd.log || true + exit 1 + fi +' + +check "iptables works" sudo iptables -L +check "iptables uses nf_tables" bash -c "iptables --version | grep nf_tables" + +check "version" docker --version +check "docker-ps" bash -c "docker ps" +check "log-exists" bash -c "ls /tmp/dockerd.log" +check "log-for-completion" bash -c "cat /tmp/dockerd.log | grep 'Daemon has completed initialization'" +check "log-contents" bash -c "cat /tmp/dockerd.log | grep 'API listen on /var/run/docker.sock'" + +# Report result +reportResults + diff --git a/test/docker-in-docker/docker_with_default_iptables_ubuntu.sh b/test/docker-in-docker/docker_with_default_iptables_ubuntu.sh new file mode 120000 index 000000000..7eb9de2c6 --- /dev/null +++ b/test/docker-in-docker/docker_with_default_iptables_ubuntu.sh @@ -0,0 +1 @@ +docker_with_default_iptables.sh \ No newline at end of file diff --git a/test/docker-in-docker/docker_with_legacy_iptables.sh b/test/docker-in-docker/docker_with_legacy_iptables.sh new file mode 100644 index 000000000..e29e10146 --- /dev/null +++ b/test/docker-in-docker/docker_with_legacy_iptables.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Feature specific tests +check "iptables works" sudo iptables -L +check "iptables uses legacy" bash -c "iptables --version | grep legacy" + +check "version" docker --version +check "docker-ps" bash -c "docker ps" +check "log-exists" bash -c "ls /tmp/dockerd.log" +check "log-for-completion" bash -c "cat /tmp/dockerd.log | grep 'Daemon has completed initialization'" +check "log-contents" bash -c "cat /tmp/dockerd.log | grep 'API listen on /var/run/docker.sock'" + +# Report result +reportResults + diff --git a/test/docker-in-docker/docker_with_legacy_iptables_ubuntu.sh b/test/docker-in-docker/docker_with_legacy_iptables_ubuntu.sh new file mode 120000 index 000000000..5b62242e3 --- /dev/null +++ b/test/docker-in-docker/docker_with_legacy_iptables_ubuntu.sh @@ -0,0 +1 @@ +docker_with_legacy_iptables.sh \ No newline at end of file diff --git a/test/docker-in-docker/scenarios.json b/test/docker-in-docker/scenarios.json index a93495b51..a4ee2a3b0 100644 --- a/test/docker-in-docker/scenarios.json +++ b/test/docker-in-docker/scenarios.json @@ -1,4 +1,61 @@ { + "docker_iptables_switch_at_install": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "docker-in-docker": { + "moby": "false", + "iptablesSwitchAtRuntime": false + } + }, + "initializeCommand": "sudo modprobe ip_tables" + }, + // DO NOT REMOVE: This scenario is used by the docker-in-docker-daemon-startup-bulk-test workflow + "docker_iptables_switch_at_runtime": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "docker-in-docker": { + "moby": "false", + "iptablesSwitchAtRuntime": true + } + }, + "initializeCommand": "sudo modprobe ip_tables" + }, + "docker_with_default_iptables": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "docker-in-docker": { + "moby": "false" + } + }, + "initializeCommand": "sudo modprobe --remove --remove-holders --wait 1000 ip_tables" + }, + "docker_with_legacy_iptables": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "docker-in-docker": { + "moby": "false" + } + }, + "initializeCommand": "sudo modprobe ip_tables" + }, + "docker_with_default_iptables_ubuntu": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "docker-in-docker": { + "moby": "false" + } + }, + "initializeCommand": "sudo modprobe --remove --remove-holders --wait 1000 ip_tables" + }, + "docker_with_legacy_iptables_ubuntu": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "docker-in-docker": { + "moby": "false" + } + }, + "initializeCommand": "sudo modprobe ip_tables" + }, "overlayfs_containerd_root": { "image": "mcr.microsoft.com/devcontainers/base:noble", "features": { @@ -215,7 +272,7 @@ } } }, - // DO NOT REMOVE: This scenario is used by the docker-in-docker-stress-test workflow + // DO NOT REMOVE: This scenario is used by the docker-in-docker-daemon-startup-bulk-test workflow "docker_with_on_create_command": { "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { From 0eb175018a2ffb84174a2cb08be617c20553aaaf Mon Sep 17 00:00:00 2001 From: Kaniska Date: Tue, 30 Jun 2026 16:21:40 +0530 Subject: [PATCH 05/21] [git] - Fixing installation from source with new version `2.55.0` (#1678) --- src/git/devcontainer-feature.json | 2 +- src/git/install.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/git/devcontainer-feature.json b/src/git/devcontainer-feature.json index 5aa83a8b1..93ea7b061 100644 --- a/src/git/devcontainer-feature.json +++ b/src/git/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "git", - "version": "1.3.5", + "version": "1.3.6", "name": "Git (from source)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/git", "description": "Install an up-to-date version of Git, built from source as needed. Useful for when you want the latest and greatest features. Auto-detects latest stable version and installs needed dependencies.", diff --git a/src/git/install.sh b/src/git/install.sh index 7ee301b7e..ed20a8ce5 100755 --- a/src/git/install.sh +++ b/src/git/install.sh @@ -318,6 +318,7 @@ cd /tmp/git-${GIT_VERSION} git_options=("prefix=/usr/local") git_options+=("sysconfdir=/etc") git_options+=("USE_LIBPCRE=YesPlease") +git_options+=("NO_RUST=YesPlease") if [ "${ADJUSTED_ID}" = "alpine" ]; then # ref. git_options+=("NO_REGEX=YesPlease") From 1863143f13d84971a56e850929ba0019e54e0ca5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:00:41 +0100 Subject: [PATCH 06/21] Bump actions/checkout from 6 to 7 (#1674) Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../docker-in-docker-daemon-startup-bulk-test.yaml | 4 ++-- .github/workflows/linter-automated.yaml | 2 +- .github/workflows/linter-manual.yaml | 2 +- .github/workflows/release.yaml | 2 +- .github/workflows/test-all.yaml | 6 +++--- .github/workflows/test-manual.yaml | 2 +- .github/workflows/test-pr-arm64.yaml | 4 ++-- .github/workflows/test-pr.yaml | 6 +++--- .github/workflows/update-aws-cli-completer-scripts.yml | 2 +- .github/workflows/update-documentation.yml | 2 +- .github/workflows/update-dotnet-install-script.yml | 2 +- .github/workflows/validate-metadata-files.yml | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/docker-in-docker-daemon-startup-bulk-test.yaml b/.github/workflows/docker-in-docker-daemon-startup-bulk-test.yaml index b7ec4339e..668879730 100644 --- a/.github/workflows/docker-in-docker-daemon-startup-bulk-test.yaml +++ b/.github/workflows/docker-in-docker-daemon-startup-bulk-test.yaml @@ -13,7 +13,7 @@ jobs: fail-fast: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli @@ -28,7 +28,7 @@ jobs: fail-fast: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli diff --git a/.github/workflows/linter-automated.yaml b/.github/workflows/linter-automated.yaml index 234f7e726..db57db29d 100644 --- a/.github/workflows/linter-automated.yaml +++ b/.github/workflows/linter-automated.yaml @@ -9,7 +9,7 @@ jobs: shellchecker: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Shell Linter uses: azohra/shell-linter@v0.8.0 diff --git a/.github/workflows/linter-manual.yaml b/.github/workflows/linter-manual.yaml index 5d4081f5a..ba72f8eaf 100644 --- a/.github/workflows/linter-manual.yaml +++ b/.github/workflows/linter-manual.yaml @@ -15,7 +15,7 @@ jobs: shellchecker: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Shell Linter uses: azohra/shell-linter@v0.8.0 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ab3c0a34b..96c95398b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,7 +13,7 @@ jobs: packages: write contents: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Publish" uses: devcontainers/action@v1 diff --git a/.github/workflows/test-all.yaml b/.github/workflows/test-all.yaml index 2c1405d9d..dd1d216a0 100644 --- a/.github/workflows/test-all.yaml +++ b/.github/workflows/test-all.yaml @@ -51,7 +51,7 @@ jobs: "mcr.microsoft.com/devcontainers/base:noble" ] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli @@ -95,7 +95,7 @@ jobs: "nix", ] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli @@ -107,7 +107,7 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli diff --git a/.github/workflows/test-manual.yaml b/.github/workflows/test-manual.yaml index 1373f5215..18dffd1b9 100644 --- a/.github/workflows/test-manual.yaml +++ b/.github/workflows/test-manual.yaml @@ -19,7 +19,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli diff --git a/.github/workflows/test-pr-arm64.yaml b/.github/workflows/test-pr-arm64.yaml index ddc761000..0de1f066b 100644 --- a/.github/workflows/test-pr-arm64.yaml +++ b/.github/workflows/test-pr-arm64.yaml @@ -51,7 +51,7 @@ jobs: - features: docker-in-docker baseImage: mcr.microsoft.com/devcontainers/base:ubuntu steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Load erofs module and verify" run: sudo modprobe erofs && grep erofs /proc/filesystems @@ -70,7 +70,7 @@ jobs: matrix: features: ${{ fromJSON(needs.detect-changes.outputs.features) }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index 8b2520752..e18291f09 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -71,7 +71,7 @@ jobs: - features: docker-outside-of-docker baseImage: mcr.microsoft.com/devcontainers/base:ubuntu steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli @@ -87,7 +87,7 @@ jobs: matrix: features: ${{ fromJSON(needs.detect-changes.outputs.features) }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli @@ -116,7 +116,7 @@ jobs: - docker_with_default_iptables - docker_with_default_iptables_ubuntu steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli diff --git a/.github/workflows/update-aws-cli-completer-scripts.yml b/.github/workflows/update-aws-cli-completer-scripts.yml index d0c119d09..fde3a29fc 100644 --- a/.github/workflows/update-aws-cli-completer-scripts.yml +++ b/.github/workflows/update-aws-cli-completer-scripts.yml @@ -12,7 +12,7 @@ jobs: contents: write pull-requests: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Run fetch-latest-completer-scripts.sh run: src/aws-cli/scripts/fetch-latest-completer-scripts.sh diff --git a/.github/workflows/update-documentation.yml b/.github/workflows/update-documentation.yml index 96c12176b..50a643fd7 100644 --- a/.github/workflows/update-documentation.yml +++ b/.github/workflows/update-documentation.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write if: "github.ref == 'refs/heads/main'" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Generate Documentation uses: devcontainers/action@v1 diff --git a/.github/workflows/update-dotnet-install-script.yml b/.github/workflows/update-dotnet-install-script.yml index 19aab95e2..16f737ff2 100644 --- a/.github/workflows/update-dotnet-install-script.yml +++ b/.github/workflows/update-dotnet-install-script.yml @@ -12,7 +12,7 @@ jobs: contents: write pull-requests: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Run fetch-latest-dotnet-install.sh run: src/dotnet/scripts/fetch-latest-dotnet-install.sh diff --git a/.github/workflows/validate-metadata-files.yml b/.github/workflows/validate-metadata-files.yml index dfb5b25f0..5b24dfbee 100644 --- a/.github/workflows/validate-metadata-files.yml +++ b/.github/workflows/validate-metadata-files.yml @@ -7,7 +7,7 @@ jobs: validate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: "Validate devcontainer-feature.json files" uses: devcontainers/action@v1 From 5e6a85458a579e737897494b313322d7c2d2ddca Mon Sep 17 00:00:00 2001 From: Kaniska Date: Tue, 30 Jun 2026 22:01:17 +0530 Subject: [PATCH 07/21] [oryx] - Fixing build issue (#1680) --- src/oryx/devcontainer-feature.json | 2 +- src/oryx/install.sh | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/oryx/devcontainer-feature.json b/src/oryx/devcontainer-feature.json index 9468222e8..5e60e3218 100644 --- a/src/oryx/devcontainer-feature.json +++ b/src/oryx/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "oryx", - "version": "2.0.0", + "version": "2.0.1", "name": "Oryx", "description": "Installs the oryx CLI", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/oryx", diff --git a/src/oryx/install.sh b/src/oryx/install.sh index eeacbec39..8a85fecd1 100755 --- a/src/oryx/install.sh +++ b/src/oryx/install.sh @@ -188,10 +188,10 @@ SOLUTION_FILE_NAME="Oryx.sln" echo "Building solution '$SOLUTION_FILE_NAME'..." cd $GIT_ORYX -${DOTNET_BINARY} build "$SOLUTION_FILE_NAME" -c Debug +${DOTNET_BINARY} build "$SOLUTION_FILE_NAME" -c Debug -p:NuGetAudit=false -${DOTNET_BINARY} publish -property:ValidateExecutableReferencesMatchSelfContained=false -r linux-x64 -o ${BUILD_SCRIPT_GENERATOR} -c Release $GIT_ORYX/src/BuildScriptGeneratorCli/BuildScriptGeneratorCli.csproj --self-contained true -${DOTNET_BINARY} publish -r linux-x64 -o ${BUILD_SCRIPT_GENERATOR} -c Release $GIT_ORYX/src/BuildServer/BuildServer.csproj --self-contained true +${DOTNET_BINARY} publish -p:NuGetAudit=false -property:ValidateExecutableReferencesMatchSelfContained=false -r linux-x64 -o ${BUILD_SCRIPT_GENERATOR} -c Release $GIT_ORYX/src/BuildScriptGeneratorCli/BuildScriptGeneratorCli.csproj --self-contained true +${DOTNET_BINARY} publish -p:NuGetAudit=false -r linux-x64 -o ${BUILD_SCRIPT_GENERATOR} -c Release $GIT_ORYX/src/BuildServer/BuildServer.csproj --self-contained true chmod a+x ${BUILD_SCRIPT_GENERATOR}/GenerateBuildScript From 8431b2dc0714188386f1ee126d8583bd617f30a3 Mon Sep 17 00:00:00 2001 From: Kaniska Date: Tue, 30 Jun 2026 22:57:40 +0530 Subject: [PATCH 08/21] [git] - Fixing issue for alpine installation from source and updating tests (#1679) * [git] - Fixing issue for alpine installation from source and updating tests * Consolidating common logic --- src/git/devcontainer-feature.json | 2 +- src/git/install.sh | 9 +++++++-- test/git/install_git_from_src.sh | 4 ++++ test/git/install_git_from_src_alpine.sh | 4 ++++ test/git/install_git_from_src_bookworm.sh | 1 + test/git/install_git_from_src_centos-7.sh | 16 ---------------- test/git/install_git_from_src_noble.sh | 4 ++++ test/git/install_git_from_src_trixie.sh | 1 + test/git/scenarios.json | 15 ++++++++++++--- test/git/utils.sh | 19 +++++++++++++++++++ 10 files changed, 53 insertions(+), 22 deletions(-) create mode 120000 test/git/install_git_from_src_bookworm.sh delete mode 100644 test/git/install_git_from_src_centos-7.sh create mode 120000 test/git/install_git_from_src_trixie.sh create mode 100644 test/git/utils.sh diff --git a/src/git/devcontainer-feature.json b/src/git/devcontainer-feature.json index 93ea7b061..46b49b27f 100644 --- a/src/git/devcontainer-feature.json +++ b/src/git/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "git", - "version": "1.3.6", + "version": "1.3.7", "name": "Git (from source)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/git", "description": "Install an up-to-date version of Git, built from source as needed. Useful for when you want the latest and greatest features. Auto-detects latest stable version and installs needed dependencies.", diff --git a/src/git/install.sh b/src/git/install.sh index ed20a8ce5..d214e3e32 100755 --- a/src/git/install.sh +++ b/src/git/install.sh @@ -263,10 +263,10 @@ elif [ "${ADJUSTED_ID}" = "alpine" ]; then ${INSTALL_CMD} add --no-cache --update curl grep make zlib-dev # ref. - check_packages asciidoc curl-dev expat-dev g++ gcc openssl-dev pcre2-dev perl-dev perl-error python3-dev tcl tk xmlto + check_packages asciidoc curl-dev expat-dev g++ gcc linux-headers openssl-dev pcre2-dev perl-dev perl-error python3-dev tcl tk xmlto elif [ "${ADJUSTED_ID}" = "rhel" ]; then - check_packages gcc libcurl-devel expat-devel gettext-devel openssl-devel perl-devel zlib-devel cmake pcre2-devel tar gzip ca-certificates + check_packages gcc make libcurl-devel expat-devel gettext-devel openssl-devel perl-devel zlib-devel cmake pcre2-devel tar gzip ca-certificates if ! type curl > /dev/null 2>&1; then check_packages curl fi @@ -325,6 +325,11 @@ if [ "${ADJUSTED_ID}" = "alpine" ]; then git_options+=("NO_GETTEXT=YesPlease") fi make -s "${git_options[@]}" all && make -s "${git_options[@]}" install 2>&1 +build_result=$? rm -rf /tmp/git-${GIT_VERSION} clean_up +if [ "${build_result}" -ne 0 ]; then + echo "(!) Failed to build and install git ${GIT_VERSION}." >&2 + exit 1 +fi echo "Done!" diff --git a/test/git/install_git_from_src.sh b/test/git/install_git_from_src.sh index d0ebaa282..4888c25fb 100644 --- a/test/git/install_git_from_src.sh +++ b/test/git/install_git_from_src.sh @@ -5,8 +5,12 @@ set -e # Optional: Import test library source dev-container-features-test-lib +# Import shared helper functions +source "$(dirname "$0")/utils.sh" + # Definition specific tests check "version" git --version +check "version-is-latest" check_git_is_latest_version check "gettext" dpkg-query -l gettext cd /tmp && git clone https://github.com/devcontainers/feature-starter.git diff --git a/test/git/install_git_from_src_alpine.sh b/test/git/install_git_from_src_alpine.sh index 2a26beec5..a9a9d7d9c 100644 --- a/test/git/install_git_from_src_alpine.sh +++ b/test/git/install_git_from_src_alpine.sh @@ -5,8 +5,12 @@ set -e # Optional: Import test library source dev-container-features-test-lib +# Import shared helper functions +source "$(dirname "$0")/utils.sh" + # Definition specific tests check "version" git --version +check "version-is-latest" check_git_is_latest_version cd /tmp && git clone https://github.com/devcontainers/feature-starter.git cd feature-starter diff --git a/test/git/install_git_from_src_bookworm.sh b/test/git/install_git_from_src_bookworm.sh new file mode 120000 index 000000000..aa0c8ade6 --- /dev/null +++ b/test/git/install_git_from_src_bookworm.sh @@ -0,0 +1 @@ +install_git_from_src.sh \ No newline at end of file diff --git a/test/git/install_git_from_src_centos-7.sh b/test/git/install_git_from_src_centos-7.sh deleted file mode 100644 index 84800b543..000000000 --- a/test/git/install_git_from_src_centos-7.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -set -e - -# Optional: Import test library -source dev-container-features-test-lib - -# Definition specific tests -check "version" git --version - -cd /tmp && git clone https://github.com/devcontainers/feature-starter.git -cd feature-starter -check "perl" bash -c "git -c grep.patternType=perl grep -q 'a.+b'" - -# Report result -reportResults diff --git a/test/git/install_git_from_src_noble.sh b/test/git/install_git_from_src_noble.sh index 337226fdc..dae12450b 100644 --- a/test/git/install_git_from_src_noble.sh +++ b/test/git/install_git_from_src_noble.sh @@ -5,8 +5,12 @@ set -e # Optional: Import test library source dev-container-features-test-lib +# Import shared helper functions +source "$(dirname "$0")/utils.sh" + # Definition specific tests check "version" git --version +check "latest version" check_git_is_latest_version check "gettext" dpkg-query -l gettext cd /tmp && git clone https://github.com/devcontainers/feature-starter.git diff --git a/test/git/install_git_from_src_trixie.sh b/test/git/install_git_from_src_trixie.sh new file mode 120000 index 000000000..aa0c8ade6 --- /dev/null +++ b/test/git/install_git_from_src_trixie.sh @@ -0,0 +1 @@ +install_git_from_src.sh \ No newline at end of file diff --git a/test/git/scenarios.json b/test/git/scenarios.json index 7feff6564..ce110d3c5 100644 --- a/test/git/scenarios.json +++ b/test/git/scenarios.json @@ -1,6 +1,6 @@ { "install_git_from_src": { - "image": "ubuntu:noble", + "image": "ubuntu:resolute", "features": { "git": { "version": "latest", @@ -53,8 +53,17 @@ } } }, - "install_git_from_src_centos-7": { - "image": "centos:centos7", + "install_git_from_src_bookworm": { + "image": "debian:bookworm", + "features": { + "git": { + "version": "latest", + "ppa": "false" + } + } + }, + "install_git_from_src_trixie": { + "image": "debian:trixie", "features": { "git": { "version": "latest", diff --git a/test/git/utils.sh b/test/git/utils.sh new file mode 100644 index 000000000..6b5b03f93 --- /dev/null +++ b/test/git/utils.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Shared helper functions for git "install from source" test scenarios. + +# Resolves the latest stable git version from GitHub +get_latest_git_version() { + curl -sSL -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/git/git/tags" \ + | grep -oP '"name":\s*"v\K[0-9]+\.[0-9]+\.[0-9]+(?=")' \ + | sort -rV \ + | head -n 1 +} + +# Verifies the installed git version matches the latest stable version on GitHub +check_git_is_latest_version() { + local latest_version installed_version + latest_version="$(get_latest_git_version)" + installed_version="$(git --version | awk '{print $3}')" + [ -n "$latest_version" ] && [ "$installed_version" = "$latest_version" ] +} From f15b529848d77c462d1bf8004c0ff465e124fdd3 Mon Sep 17 00:00:00 2001 From: Daniel Meilak <32960789+daniel-meilak@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:49:12 +0200 Subject: [PATCH 09/21] Fix desktop-lite ALSA package selection (#1676) * Fix desktop-lite ALSA package selection Select the first installable ALSA package by apt candidate availability so Ubuntu releases before and after the t64 transition continue to work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Order newer packages first Co-authored-by: nicholas Krul * Use symlink for duplicate desktop-lite ALSA test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: nicholas Krul Co-authored-by: Kaniska --- src/desktop-lite/devcontainer-feature.json | 2 +- src/desktop-lite/install.sh | 26 ++++++++++----- test/desktop-lite/check_asound_package.sh | 33 +++++++++++++++++++ test/desktop-lite/scenarios.json | 12 +++++++ test/desktop-lite/test.sh | 22 ++----------- .../test_asound_package_ubuntu_2204.sh | 1 + .../test_asound_package_ubuntu_2604.sh | 14 ++++++++ 7 files changed, 82 insertions(+), 28 deletions(-) create mode 100644 test/desktop-lite/check_asound_package.sh create mode 120000 test/desktop-lite/test_asound_package_ubuntu_2204.sh create mode 100755 test/desktop-lite/test_asound_package_ubuntu_2604.sh diff --git a/src/desktop-lite/devcontainer-feature.json b/src/desktop-lite/devcontainer-feature.json index b87e2bc02..891ac6d73 100644 --- a/src/desktop-lite/devcontainer-feature.json +++ b/src/desktop-lite/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "desktop-lite", - "version": "1.2.9", + "version": "1.2.10", "name": "Light-weight Desktop", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/desktop-lite", "description": "Adds a lightweight Fluxbox based desktop to the container that can be accessed using a VNC viewer or the web. GUI-based commands executed from the built-in VS code terminal will open on the desktop automatically.", diff --git a/src/desktop-lite/install.sh b/src/desktop-lite/install.sh index 822818723..0d6d6e31d 100755 --- a/src/desktop-lite/install.sh +++ b/src/desktop-lite/install.sh @@ -168,6 +168,19 @@ check_packages() { fi } +find_available_package() { + local candidate + local package_name + for package_name in "$@"; do + candidate="$(apt-cache policy "${package_name}" | awk '/Candidate:/ {print $2}')" + if [ -n "${candidate}" ] && [ "${candidate}" != "(none)" ]; then + echo "${package_name}" + return 0 + fi + done + return 1 +} + ########################## # Install starts here # ########################## @@ -199,15 +212,12 @@ fi # Install X11, fluxbox and VS Code dependencies check_packages ${package_list} -# if Ubuntu-24.04, noble(numbat) / Debian-13, trixie found, then will install libasound2-dev instead of libasound2. -# this change is temporary, https://packages.ubuntu.com/noble/libasound2 will switch to libasound2 once it is available for Ubuntu-24.04, noble(numbat) -. /etc/os-release -if { [ "${ID}" = "ubuntu" ] && [ "${VERSION_CODENAME}" = "noble" ]; } || { [ "${ID}" = "debian" ] && [ "${VERSION_CODENAME}" = "trixie" ]; }; then - echo "Detected Noble (Ubuntu 24.04) or Trixie (Debian). Installing libasound2-dev package..." - check_packages "libasound2-dev" -else - check_packages "libasound2" +if ! alsa_package="$(find_available_package libasound2t64 libasound2 libasound2-dev)"; then + echo "(!) No supported ALSA package found. Tried: libasound2, libasound2t64, libasound2-dev." >&2 + exit 1 fi +echo "Installing ${alsa_package} package..." +check_packages "${alsa_package}" # On newer versions of Ubuntu (22.04), # we need an additional package that isn't provided in earlier versions diff --git a/test/desktop-lite/check_asound_package.sh b/test/desktop-lite/check_asound_package.sh new file mode 100644 index 000000000..112dac197 --- /dev/null +++ b/test/desktop-lite/check_asound_package.sh @@ -0,0 +1,33 @@ +checkOSPackage() { + PACKAGE_NAME=$1 + # Check if the package exists and retrieve its exact version + if [ "$(dpkg-query -W -f='${Status}' "$PACKAGE_NAME" 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then + echo "โœ… Package '$PACKAGE_NAME' is installed." + return 0 + else + echo "โŒ Package '$PACKAGE_NAME' is not installed." + return 1 + fi +} + +findAvailableOSPackage() { + local candidate + local package_name + for package_name in "$@"; do + candidate="$(apt-cache policy "${package_name}" | awk '/Candidate:/ {print $2}')" + if [ -n "${candidate}" ] && [ "${candidate}" != "(none)" ]; then + echo "${package_name}" + return 0 + fi + done + return 1 +} + +checkAsoundPackage() { + local alsa_package + if ! alsa_package="$(findAvailableOSPackage libasound2 libasound2t64 libasound2-dev)"; then + echo "No supported ALSA package found in apt indexes." >&2 + exit 1 + fi + check "alsa-package-installed-${alsa_package}" checkOSPackage "${alsa_package}" +} diff --git a/test/desktop-lite/scenarios.json b/test/desktop-lite/scenarios.json index f8719acc6..fcb804ff5 100644 --- a/test/desktop-lite/scenarios.json +++ b/test/desktop-lite/scenarios.json @@ -45,6 +45,18 @@ "desktop-lite": {} } }, + "test_asound_package_ubuntu_2204": { + "image": "ubuntu:22.04", + "features": { + "desktop-lite": {} + } + }, + "test_asound_package_ubuntu_2604": { + "image": "ubuntu:26.04", + "features": { + "desktop-lite": {} + } + }, "test_desktop_init_exec_passthrough": { "image": "ubuntu:noble", "features": { diff --git a/test/desktop-lite/test.sh b/test/desktop-lite/test.sh index 32eab53c4..a22c49532 100755 --- a/test/desktop-lite/test.sh +++ b/test/desktop-lite/test.sh @@ -10,30 +10,14 @@ echoStderr() echo "$@" 1>&2 } -checkOSPackage() { - LABEL=$1 - PACKAGE_NAME=$2 - echo -e "\n๐Ÿงช Testing $LABEL" - # Check if the package exists and retrieve its exact version - if [ "$(dpkg-query -W -f='${Status}' "$PACKAGE_NAME" 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then - echo "โœ… Package '$PACKAGE_NAME' is installed." - exit 0 - else - echo "โŒ Package '$PACKAGE_NAME' is not installed." - exit 1 - fi -} +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/check_asound_package.sh" check "desktop-init-exists" bash -c "ls /usr/local/share/desktop-init.sh" check "log-exists" bash -c "ls /tmp/container-init.log" check "fluxbox-exists" bash -c "ls -la ~/.fluxbox" -. /etc/os-release -if [ "${VERSION_CODENAME}" = "noble" ] || [ "${VERSION_CODENAME}" = "trixie" ]; then - checkOSPackage "if libasound2-dev exists !" "libasound2-dev" -else - checkOSPackage "if libasound2 exists !" "libasound2" -fi +checkAsoundPackage # Report result reportResults \ No newline at end of file diff --git a/test/desktop-lite/test_asound_package_ubuntu_2204.sh b/test/desktop-lite/test_asound_package_ubuntu_2204.sh new file mode 120000 index 000000000..297c107db --- /dev/null +++ b/test/desktop-lite/test_asound_package_ubuntu_2204.sh @@ -0,0 +1 @@ +test_asound_package_ubuntu_2604.sh \ No newline at end of file diff --git a/test/desktop-lite/test_asound_package_ubuntu_2604.sh b/test/desktop-lite/test_asound_package_ubuntu_2604.sh new file mode 100755 index 000000000..150a806ed --- /dev/null +++ b/test/desktop-lite/test_asound_package_ubuntu_2604.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${script_dir}/check_asound_package.sh" + +checkAsoundPackage + +# Report result +reportResults From 6c375f1d65510836760bef052f4614a0df974946 Mon Sep 17 00:00:00 2001 From: Paul Taylor <178183+trxcllnt@users.noreply.github.com> Date: Wed, 8 Jul 2026 04:40:37 -0700 Subject: [PATCH 10/21] Fix installing git-core PPA in Ubuntu 26.04 (#1675) * dearmor git-core ppa key for Ubuntu Resolute * add git-core ppa tests for ubuntu noble and resolute * make new tests into symlinks --------- Co-authored-by: Kaniska --- src/git/devcontainer-feature.json | 2 +- src/git/install.sh | 28 +++++++++++------------ test/git/install_git_from_ppa_noble.sh | 1 + test/git/install_git_from_ppa_resolute.sh | 1 + test/git/install_git_from_src_resolute.sh | 1 + test/git/scenarios.json | 27 ++++++++++++++++++++++ 6 files changed, 45 insertions(+), 15 deletions(-) create mode 120000 test/git/install_git_from_ppa_noble.sh create mode 120000 test/git/install_git_from_ppa_resolute.sh create mode 120000 test/git/install_git_from_src_resolute.sh diff --git a/src/git/devcontainer-feature.json b/src/git/devcontainer-feature.json index 46b49b27f..1aa7e237b 100644 --- a/src/git/devcontainer-feature.json +++ b/src/git/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "git", - "version": "1.3.7", + "version": "1.3.8", "name": "Git (from source)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/git", "description": "Install an up-to-date version of Git, built from source as needed. Useful for when you want the latest and greatest features. Auto-detects latest stable version and installs needed dependencies.", diff --git a/src/git/install.sh b/src/git/install.sh index d214e3e32..e289ce16b 100755 --- a/src/git/install.sh +++ b/src/git/install.sh @@ -110,12 +110,8 @@ get_gpg_key_servers() { # Import the specified key in a variable name passed in as receive_gpg_keys() { - local keys=${!1} - local keyring_args="" - if [ ! -z "$2" ]; then - mkdir -p "$(dirname \"$2\")" - keyring_args="--no-default-keyring --keyring $2" - fi + local -a keys="(${!1})" + mkdir -p "$(dirname "$2")" # Install curl if ! type curl > /dev/null 2>&1; then @@ -133,13 +129,17 @@ receive_gpg_keys() { set +e until [ "${gpg_ok}" = "true" ] || [ "${retry_count}" -eq "5" ]; do - echo "(*) Downloading GPG key..." - ( echo "${keys}" | xargs -n 1 gpg -q ${keyring_args} --recv-keys) 2>&1 && gpg_ok="true" - if [ "${gpg_ok}" != "true" ]; then - echo "(*) Failed getting key, retrying in 10s..." - (( retry_count++ )) - sleep 10s - fi + for key in "${keys[@]}"; do + echo "(*) Downloading GPG key '${key}'..." + gpg --recv-keys "${key}" \ + && gpg --export "${key}" | gpg --dearmor --yes -o "$2" \ + && gpg_ok="true" + if [ "${gpg_ok}" != "true" ]; then + echo "(*) Failed getting key, retrying in 10s..." + (( retry_count++ )) + sleep 10s + fi + done done set -e if [ "${gpg_ok}" = "false" ]; then @@ -275,7 +275,7 @@ elif [ "${ADJUSTED_ID}" = "rhel" ]; then fi if ! type awk > /dev/null 2>&1; then check_packages gawk - fi + fi if [ $ID = "mariner" ]; then check_packages glibc-devel kernel-headers binutils fi diff --git a/test/git/install_git_from_ppa_noble.sh b/test/git/install_git_from_ppa_noble.sh new file mode 120000 index 000000000..408beff37 --- /dev/null +++ b/test/git/install_git_from_ppa_noble.sh @@ -0,0 +1 @@ +install_git_from_ppa_jammy.sh \ No newline at end of file diff --git a/test/git/install_git_from_ppa_resolute.sh b/test/git/install_git_from_ppa_resolute.sh new file mode 120000 index 000000000..408beff37 --- /dev/null +++ b/test/git/install_git_from_ppa_resolute.sh @@ -0,0 +1 @@ +install_git_from_ppa_jammy.sh \ No newline at end of file diff --git a/test/git/install_git_from_src_resolute.sh b/test/git/install_git_from_src_resolute.sh new file mode 120000 index 000000000..1e44e792a --- /dev/null +++ b/test/git/install_git_from_src_resolute.sh @@ -0,0 +1 @@ +install_git_from_src_noble.sh \ No newline at end of file diff --git a/test/git/scenarios.json b/test/git/scenarios.json index ce110d3c5..d47f0bb6e 100644 --- a/test/git/scenarios.json +++ b/test/git/scenarios.json @@ -44,6 +44,33 @@ } } }, + "install_git_from_ppa_noble": { + "image": "ubuntu:noble", + "features": { + "git": { + "version": "latest", + "ppa": "true" + } + } + }, + "install_git_from_src_resolute": { + "image": "ubuntu:resolute", + "features": { + "git": { + "version": "latest", + "ppa": "false" + } + } + }, + "install_git_from_ppa_resolute": { + "image": "ubuntu:resolute", + "features": { + "git": { + "version": "latest", + "ppa": "true" + } + } + }, "install_git_from_src_bullseye": { "image": "debian:bullseye", "features": { From 0f547996943a66f51c28cae151fec87907b27ee9 Mon Sep 17 00:00:00 2001 From: Bobby Reynolds <37971212+reynoldsbd@users.noreply.github.com> Date: Tue, 14 Jul 2026 06:51:42 -0700 Subject: [PATCH 11/21] fix(rust): support Azure Linux base images (#1685) --- src/rust/NOTES.md | 4 ++-- src/rust/README.md | 4 ++-- src/rust/devcontainer-feature.json | 2 +- src/rust/install.sh | 7 ++++--- test/rust/rust_with_azurelinux.sh | 31 ++++++++++++++++++++++++++++++ test/rust/scenarios.json | 10 ++++++++++ 6 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 test/rust/rust_with_azurelinux.sh diff --git a/src/rust/NOTES.md b/src/rust/NOTES.md index 1f01e6e52..68d170302 100644 --- a/src/rust/NOTES.md +++ b/src/rust/NOTES.md @@ -2,8 +2,8 @@ ## OS Support -This Feature should work on recent versions of Debian/Ubuntu, RedHat Enterprise Linux, Fedora, Alma, RockyLinux -and Mariner distributions with the `apt`, `yum`, `dnf`, `microdnf` and `tdnf` package manager installed. +This Feature should work on recent versions of Debian/Ubuntu, RedHat Enterprise Linux, Fedora, Alma, RockyLinux, +Mariner and Azure Linux distributions with the `apt`, `yum`, `dnf`, `microdnf` and `tdnf` package manager installed. **Note:** Alpine is not supported because the rustup-init binary requires glibc to run, but Alpine Linux does not include `glibc` diff --git a/src/rust/README.md b/src/rust/README.md index ef0bc2311..eca22932c 100644 --- a/src/rust/README.md +++ b/src/rust/README.md @@ -32,8 +32,8 @@ Installs Rust, common Rust utilities, and their required dependencies ## OS Support -This Feature should work on recent versions of Debian/Ubuntu, RedHat Enterprise Linux, Fedora, Alma, RockyLinux -and Mariner distributions with the `apt`, `yum`, `dnf`, `microdnf` and `tdnf` package manager installed. +This Feature should work on recent versions of Debian/Ubuntu, RedHat Enterprise Linux, Fedora, Alma, RockyLinux, +Mariner and Azure Linux distributions with the `apt`, `yum`, `dnf`, `microdnf` and `tdnf` package manager installed. **Note:** Alpine is not supported because the rustup-init binary requires glibc to run, but Alpine Linux does not include `glibc` diff --git a/src/rust/devcontainer-feature.json b/src/rust/devcontainer-feature.json index 88b64daed..d8d399cde 100644 --- a/src/rust/devcontainer-feature.json +++ b/src/rust/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "rust", - "version": "1.5.0", + "version": "1.5.1", "name": "Rust", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/rust", "description": "Installs Rust, common Rust utilities, and their required dependencies", diff --git a/src/rust/install.sh b/src/rust/install.sh index 99a7ba8f5..56bb35ea8 100755 --- a/src/rust/install.sh +++ b/src/rust/install.sh @@ -30,7 +30,7 @@ if [ "${ID}" = "debian" ] || [ "${ID_LIKE}" = "debian" ]; then ADJUSTED_ID="debian" elif [ "${ID}" = "alpine" ]; then ADJUSTED_ID="alpine" -elif [[ "${ID}" = "rhel" || "${ID}" = "fedora" || "${ID}" = "mariner" || "${ID_LIKE}" = *"rhel"* || "${ID_LIKE}" = *"fedora"* || "${ID_LIKE}" = *"mariner"* ]]; then +elif [[ "${ID}" = "rhel" || "${ID}" = "fedora" || "${ID}" = "azurelinux" || "${ID}" = "mariner" || "${ID_LIKE}" = *"rhel"* || "${ID_LIKE}" = *"fedora"* || "${ID_LIKE}" = *"azurelinux"* || "${ID_LIKE}" = *"mariner"* ]]; then ADJUSTED_ID="rhel" VERSION_CODENAME="${ID}${VERSION_ID}" else @@ -264,6 +264,7 @@ check_packages() { "python3-minimal") packages[$i]="python3" ;; "libpython3.*") packages[$i]="python3-devel" ;; "gnupg2") packages[$i]="gnupg" ;; + "passwd") packages[$i]="shadow-utils" ;; esac ;; esac @@ -303,7 +304,7 @@ export DEBIAN_FRONTEND=noninteractive # Install curl, lldb, python3-minimal,libpython and rust dependencies if missing echo "Installing required dependencies..." -check_packages curl ca-certificates gcc libc6-dev gnupg2 git +check_packages curl ca-certificates gcc libc6-dev gnupg2 git passwd # Install optional dependencies (continue if they fail) case "$PKG_MANAGER" in @@ -315,7 +316,7 @@ case "$PKG_MANAGER" in ;; tdnf) check_packages python3 python3-devel || true - # LLDB might not be available in Photon/Mariner + # LLDB might not be available in Photon/Mariner/Azure Linux ;; esac diff --git a/test/rust/rust_with_azurelinux.sh b/test/rust/rust_with_azurelinux.sh new file mode 100644 index 000000000..ac940136f --- /dev/null +++ b/test/rust/rust_with_azurelinux.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Helper function to check component is installed +check_component_installed() { + local component=$1 + if rustup component list | grep -q "${component}.*installed"; then + return 0 # Component is installed (success) + else + return 1 # Component is not installed (failure) + fi +} + +# Definition specific tests +check "cargo version" cargo --version +check "rustc version" rustc --version +check "correct rust version" rustup target list | grep aarch64-unknown-linux-gnu + +# Check that all specified extended components are installed +check "rust-analyzer is installed" check_component_installed "rust-analyzer" +check "rust-src is installed" check_component_installed "rust-src" +check "rustfmt is installed" check_component_installed "rustfmt" +check "clippy is installed" check_component_installed "clippy" +check "rust-docs is installed" check_component_installed "rust-docs" + +# Report result +reportResults \ No newline at end of file diff --git a/test/rust/scenarios.json b/test/rust/scenarios.json index 21e347947..87c93bfa7 100644 --- a/test/rust/scenarios.json +++ b/test/rust/scenarios.json @@ -134,5 +134,15 @@ "components": "rust-analyzer,rust-src,rustfmt,clippy,rust-docs" } } + }, + "rust_with_azurelinux": { + "image": "mcr.microsoft.com/azurelinux/base/core:3.0", + "features": { + "rust": { + "version": "latest", + "targets": "aarch64-unknown-linux-gnu", + "components": "rust-analyzer,rust-src,rustfmt,clippy,rust-docs" + } + } } } From 765e8ebd8f8012fb740cd7b41483a745bcedd212 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:56:43 +0100 Subject: [PATCH 12/21] Fix terraform feature OpenPGP error on Ubuntu 26.04 (resolute) (#1683) * Initial plan * Extend GPG workaround to Ubuntu 26.04 (resolute) and add tests * Use devcontainers base:resolute image for terraform resolute tests * Bump terraform feature version to 1.4.4 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- src/terraform/devcontainer-feature.json | 2 +- src/terraform/install.sh | 6 +++--- test/terraform/install_in_ubuntu_resolute.sh | 17 ++++++++++++++++ .../install_in_ubuntu_resolute_sentinel.sh | 20 +++++++++++++++++++ test/terraform/scenarios.json | 16 +++++++++++++++ 5 files changed, 57 insertions(+), 4 deletions(-) create mode 100755 test/terraform/install_in_ubuntu_resolute.sh create mode 100755 test/terraform/install_in_ubuntu_resolute_sentinel.sh diff --git a/src/terraform/devcontainer-feature.json b/src/terraform/devcontainer-feature.json index f9ebcbee8..37a60db26 100644 --- a/src/terraform/devcontainer-feature.json +++ b/src/terraform/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "terraform", - "version": "1.4.3", + "version": "1.4.4", "name": "Terraform, tflint, and TFGrunt", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/terraform", "description": "Installs the Terraform CLI and optionally TFLint and Terragrunt. Auto-detects latest version and installs needed dependencies.", diff --git a/src/terraform/install.sh b/src/terraform/install.sh index 8c4755ebe..8bc79107c 100755 --- a/src/terraform/install.sh +++ b/src/terraform/install.sh @@ -19,8 +19,8 @@ INSTALL_SENTINEL=${INSTALLSENTINEL:-false} INSTALL_TFSEC=${INSTALLTFSEC:-false} INSTALL_TERRAFORM_DOCS=${INSTALLTERRAFORMDOCS:-false} CUSTOM_DOWNLOAD_SERVER="${CUSTOMDOWNLOADSERVER:-""}" -# This is because ubuntu noble and debian trixie don't support the old format of GPG keys and validation -NEW_GPG_CODENAMES="trixie noble" +# This is because ubuntu noble, ubuntu resolute and debian trixie don't support the old format of GPG keys and validation +NEW_GPG_CODENAMES="trixie noble resolute" TERRAFORM_SHA256="${TERRAFORM_SHA256:-"automatic"}" TFLINT_SHA256="${TFLINT_SHA256:-"automatic"}" @@ -52,7 +52,7 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi -# Detect Ubuntu Noble or Debian Trixie and use new repo setup, else use legacy GPG logic +# Detect Ubuntu Noble, Ubuntu Resolute or Debian Trixie and use new repo setup, else use legacy GPG logic IS_GPG_NEW=0 . /etc/os-release if [[ "${NEW_GPG_CODENAMES}" == *"${VERSION_CODENAME}"* ]]; then diff --git a/test/terraform/install_in_ubuntu_resolute.sh b/test/terraform/install_in_ubuntu_resolute.sh new file mode 100755 index 000000000..8fa4cacb9 --- /dev/null +++ b/test/terraform/install_in_ubuntu_resolute.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +# Import test library +source dev-container-features-test-lib + +# Check to make sure the user is vscode +check "user is vscode" whoami | grep vscode + +# Check if terraform was installed correctly +check "terraform installed" terraform --version + +check "tflint" tflint --version + +# Report results +reportResults diff --git a/test/terraform/install_in_ubuntu_resolute_sentinel.sh b/test/terraform/install_in_ubuntu_resolute_sentinel.sh new file mode 100755 index 000000000..32c76bbe4 --- /dev/null +++ b/test/terraform/install_in_ubuntu_resolute_sentinel.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e + +# Import test library for `check` command +source dev-container-features-test-lib + +# Check to make sure the user is vscode +check "user is vscode" whoami | grep vscode + +# Check if terraform was installed correctly +check "terraform installed" terraform --version + +check "tflint" tflint --version + +# Sentinel specific tests +check "sentinel" sentinel --version + +# Report result +reportResults diff --git a/test/terraform/scenarios.json b/test/terraform/scenarios.json index 09bb0f598..393bd3303 100644 --- a/test/terraform/scenarios.json +++ b/test/terraform/scenarios.json @@ -31,6 +31,22 @@ } } }, + "install_in_ubuntu_resolute": { + "image": "mcr.microsoft.com/devcontainers/base:resolute", + "features": { + "terraform": { + "version": "latest" + } + } + }, + "install_in_ubuntu_resolute_sentinel": { + "image": "mcr.microsoft.com/devcontainers/base:resolute", + "features": { + "terraform": { + "installSentinel": true + } + } + }, "install_sentinel": { "image": "mcr.microsoft.com/devcontainers/base:jammy", "features": { From 40aeb53ad77842bc30c9a1adb9ad28a41b753153 Mon Sep 17 00:00:00 2001 From: Venkumahanti Subhankar Date: Tue, 28 Jul 2026 15:49:25 +0530 Subject: [PATCH 13/21] docs(node): document pre-bundled items and how to opt out (#1695) * docs(node): document pre-bundled items and how to opt out Added notes on excluding pre-bundled items and VS Code extensions. * Fix formatting in NOTES.md Corrected formatting and punctuation in the NOTES.md file. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/node/NOTES.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/node/NOTES.md b/src/node/NOTES.md index 506fa1b4e..9f1c1d3bc 100644 --- a/src/node/NOTES.md +++ b/src/node/NOTES.md @@ -25,3 +25,27 @@ Debian/Ubuntu, RedHat Enterprise Linux, Fedora, Alma, and Rocky Linux distributi **Note**: RedHat 7 Family (RedHat, CentOS, etc.) must use Node versions less than 18 due to its system libraries and long-term support (LTS) policies. `bash` is required to execute the `install.sh` script. + +## Pre-bundled items + +> [!NOTE] +> Beyond the core install, this feature also sets up a few items by default for convenience โ€” recommended VS Code extensions (such as a linter) and supporting tools. This is intentional behavior shared across features in this repository. + +## Excluding pre-bundled items + +Exclude a bundled **VS Code extension** by prefixing its ID with `-`, or (when supported by a feature option) disable a bundled **tool** by setting its version option to `none` (for example, `pnpmVersion`: `none`): + +```json +{ + "features": { + "ghcr.io/devcontainers/features/node:2": { + "pnpmVersion": "none" + } + }, + "customizations": { + "vscode": { + "extensions": [ "-dbaeumer.vscode-eslint" ] + } + } +} +``` From 529a88d08e10671b5e61c48ade6904a02b867779 Mon Sep 17 00:00:00 2001 From: Venkumahanti Subhankar Date: Tue, 28 Jul 2026 15:51:09 +0530 Subject: [PATCH 14/21] fix(nix): Resolve PATH mismatch for packages option in multi-user mode (#1691) * fix(nix): Resolve PATH mismatch for packages option in multi-user mode * fix(nix): align package installs with the active multi-user profile --- src/nix/install.sh | 6 ++---- src/nix/post-install-steps.sh | 14 +++++++++++--- test/nix/packages.sh | 1 + test/nix/scenarios.json | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/nix/install.sh b/src/nix/install.sh index 0030c2b18..ca19f658a 100755 --- a/src/nix/install.sh +++ b/src/nix/install.sh @@ -113,10 +113,8 @@ fi chmod +x,o+r ${FEATURE_DIR} ${FEATURE_DIR}/post-install-steps.sh if [ "${MULTIUSER}" = "true" ]; then /usr/local/share/nix-entrypoint.sh - su ${USERNAME} -c " - . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh - ${FEATURE_DIR}/post-install-steps.sh - " + . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh + NIX_FEATURE_INSTALL_PROFILE=/nix/var/nix/profiles/default ${FEATURE_DIR}/post-install-steps.sh else su ${USERNAME} -c " . \$HOME/.nix-profile/etc/profile.d/nix.sh diff --git a/src/nix/post-install-steps.sh b/src/nix/post-install-steps.sh index 68f93a391..94cfed8a3 100755 --- a/src/nix/post-install-steps.sh +++ b/src/nix/post-install-steps.sh @@ -2,6 +2,14 @@ set -e echo "(*) Executing post-installation steps..." +# In multi-user mode, install into the default profile that is on PATH. +NIX_ENV_PROFILE_ARGS=() +NIX_PROFILE_INSTALL_ARGS=() +if [ -n "${NIX_FEATURE_INSTALL_PROFILE}" ]; then + NIX_ENV_PROFILE_ARGS=(-p "${NIX_FEATURE_INSTALL_PROFILE}") + NIX_PROFILE_INSTALL_ARGS=(--profile "${NIX_FEATURE_INSTALL_PROFILE}") +fi + # if not starts with "nixpkgs." add it as prefix to package name add_nixpkgs_prefix() { local packages=$1 @@ -20,17 +28,17 @@ if [ ! -z "${PACKAGES}" ] && [ "${PACKAGES}" != "none" ]; then if [ "${USEATTRIBUTEPATH}" = "true" ]; then PACKAGES=$(add_nixpkgs_prefix "$PACKAGES") echo "Installing packages \"${PACKAGES}\" in profile..." - nix-env -iA ${PACKAGES} + nix-env "${NIX_ENV_PROFILE_ARGS[@]}" -iA ${PACKAGES} else echo "Installing packages \"${PACKAGES}\" in profile..." - nix-env --install ${PACKAGES} + nix-env "${NIX_ENV_PROFILE_ARGS[@]}" --install ${PACKAGES} fi fi # Install Nix flake in profile if specified if [ ! -z "${FLAKEURI}" ] && [ "${FLAKEURI}" != "none" ]; then echo "Installing flake ${FLAKEURI} in profile..." - nix profile install "${FLAKEURI}" + nix profile install "${NIX_PROFILE_INSTALL_ARGS[@]}" "${FLAKEURI}" fi nix-collect-garbage --delete-old diff --git a/test/nix/packages.sh b/test/nix/packages.sh index ad896e9e0..c59a6e6d9 100755 --- a/test/nix/packages.sh +++ b/test/nix/packages.sh @@ -29,6 +29,7 @@ check "nix-env" type nix-env check "vim_installed" type vim check "node_installed" type node check "yarn_installed" type yarn +check "vim_in_default_profile" bash -lc "nix-env -p /nix/var/nix/profiles/default -q | grep -q '^vim'" # Report result # If any of the checks above exited with a non-zero exit code, the test will fail. diff --git a/test/nix/scenarios.json b/test/nix/scenarios.json index 38eed0268..3fb839629 100644 --- a/test/nix/scenarios.json +++ b/test/nix/scenarios.json @@ -86,7 +86,7 @@ "remoteUser": "vscode", "features": { "nix": { - "packages": "nodePackages.nodejs,nixpkgs.vim,nixpkgs.yarn", + "packages": "nodejs,nixpkgs.vim,nixpkgs.yarn", "useAttributePath": true } } From 99a3f1c39fb6771640100bfcbbb4ef5de1e4aa1e Mon Sep 17 00:00:00 2001 From: Venkumahanti Subhankar Date: Tue, 28 Jul 2026 16:20:33 +0530 Subject: [PATCH 15/21] fix(java): Add retry logic for transient SDKMAN bootstrap failures (#1688) * Implement retry logic for SDK installation Added a retry mechanism for SDK installation and SDKMAN CLI installation. * Update Java version from 1.8.0 to 1.8.1 Mandatory minor bump for fixing the sdkman transient error * Update expected Java version in installation script Java version updated due to upstream update from sdkman. * Fix string quotes in install_latest_version.sh * Change curl option from -sSL to -fsSL in install.sh --- src/java/devcontainer-feature.json | 2 +- src/java/install.sh | 28 ++++++++++++++++++++++++++-- test/java/install_latest_version.sh | 4 ++-- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/java/devcontainer-feature.json b/src/java/devcontainer-feature.json index c82718a49..3a1170df7 100644 --- a/src/java/devcontainer-feature.json +++ b/src/java/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "java", - "version": "1.8.0", + "version": "1.8.1", "name": "Java (via SDKMAN!)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/java", "description": "Installs Java, SDKMAN! (if not installed), and needed dependencies.", diff --git a/src/java/install.sh b/src/java/install.sh index 988460307..a142ab9e3 100644 --- a/src/java/install.sh +++ b/src/java/install.sh @@ -195,6 +195,29 @@ updaterc() { fi } +run_with_retries() { + local max_attempts="$1" + local wait_seconds="$2" + local operation="$3" + local attempt=1 + shift 3 + + until "$@"; do + if [ "${attempt}" -ge "${max_attempts}" ]; then + echo "(!) ${operation} failed after ${max_attempts} attempts." + return 1 + fi + + echo "(*) ${operation} failed on attempt ${attempt}. Retrying in ${wait_seconds}s..." + attempt=$((attempt + 1)) + sleep "${wait_seconds}" + done +} + +install_sdkman_cli() { + bash -o pipefail -c 'curl -fsSL "https://get.sdkman.io?rcupdate=false" | bash' +} + find_version_list() { prefix="$1" suffix="$2" @@ -284,7 +307,8 @@ sdk_install() { JAVA_VERSION=${requested_version} fi - su ${USERNAME} -c "umask 0002 && . ${SDKMAN_DIR}/bin/sdkman-init.sh && sdk install ${install_type} ${requested_version} && sdk flush archives && sdk flush temp" + run_with_retries 5 10 "Installing ${install_type} ${requested_version} via SDKMAN" \ + su ${USERNAME} -c "umask 0002 && . ${SDKMAN_DIR}/bin/sdkman-init.sh && sdk install ${install_type} ${requested_version} && sdk flush archives && sdk flush temp" } export DEBIAN_FRONTEND=noninteractive @@ -323,7 +347,7 @@ if [ ! -d "${SDKMAN_DIR}" ]; then if [ "${ADJUSTED_ID}" = "rhel" ] && [ "${MAJOR_VERSION_ID}" = "8" ]; then export SDKMAN_NATIVE_VERSION="false" fi - curl -sSL "https://get.sdkman.io?rcupdate=false" | bash + run_with_retries 5 10 "Installing SDKMAN" install_sdkman_cli # For RHEL 8 systems, also disable native CLI in config file and remove native binaries if [ "${ADJUSTED_ID}" = "rhel" ] && [ "${MAJOR_VERSION_ID}" = "8" ]; then # Disable native CLI in config to prevent future usage diff --git a/test/java/install_latest_version.sh b/test/java/install_latest_version.sh index 03175e767..2c8852970 100644 --- a/test/java/install_latest_version.sh +++ b/test/java/install_latest_version.sh @@ -8,8 +8,8 @@ source dev-container-features-test-lib echo 'public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } }' > HelloWorld.java javac HelloWorld.java -check "hello world" /bin/bash -c "java HelloWorld | grep "Hello, World!"" -check "java version latest installed" grep "25" <(java --version) +check "hello world" /bin/bash -c 'java HelloWorld | grep "Hello, World!"' +check "java version latest installed" grep "26" <(java --version) # Report result reportResults From 4170bca4d08a2be5d96a33251a45a0027cc5cacb Mon Sep 17 00:00:00 2001 From: Kazuma Watanabe Date: Tue, 28 Jul 2026 23:03:35 +0900 Subject: [PATCH 16/21] terraform: Add support for GitHub Attestations in TFLint installation (#1589) * terraform: Add support for GitHub Attestations in TFLint installation * terraform: Pin TFLint version in tflint_fallback_test * terraform: Bump version to 1.4.3 * terraform: Update tflint_fallback_test for the latest version --------- Co-authored-by: Kaniska Co-authored-by: Abdurrahmaan Iqbal --- src/terraform/devcontainer-feature.json | 7 +- src/terraform/install.sh | 70 ++++++++++++++------ test/terraform/scenarios.json | 4 +- test/terraform/tflint_fallback_test.sh | 85 ++++++++++++++++--------- 4 files changed, 114 insertions(+), 52 deletions(-) diff --git a/src/terraform/devcontainer-feature.json b/src/terraform/devcontainer-feature.json index 37a60db26..29d3efb30 100644 --- a/src/terraform/devcontainer-feature.json +++ b/src/terraform/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "terraform", - "version": "1.4.4", + "version": "1.4.5", "name": "Terraform, tflint, and TFGrunt", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/terraform", "description": "Installs the Terraform CLI and optionally TFLint and Terragrunt. Auto-detects latest version and installs needed dependencies.", @@ -79,6 +79,11 @@ } } }, + "dependsOn": { + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "latest" + } + }, "installsAfter": [ "ghcr.io/devcontainers/features/common-utils" ] diff --git a/src/terraform/install.sh b/src/terraform/install.sh index 8bc79107c..ef0c73e5d 100755 --- a/src/terraform/install.sh +++ b/src/terraform/install.sh @@ -460,6 +460,23 @@ install_tflint() { curl -sSL -o /tmp/tf-downloads/${TFLINT_FILENAME} https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/${TFLINT_FILENAME} } +verify_tflint_attestations() { + local checksums=$1 + local checksums_sha256=$(sha256sum "$checksums" | cut -d " " -f 1) + + check_packages jq + + curl -L -f "https://api.github.com/repos/terraform-linters/tflint/attestations/sha256:${checksums_sha256}" > attestation.json + curl_exit_code=$? + if [ $curl_exit_code -ne 0 ]; then + echo "(*) Failed to fetch GitHub Attestations for tflint checksums" + return 1 + fi + + jq ".attestations[].bundle" attestation.json > bundle.jsonl + gh at verify "$checksums" -R terraform-linters/tflint -b bundle.jsonl +} + if [ "${TFLINT_VERSION}" != "none" ]; then echo "Downloading tflint..." TFLINT_FILENAME="tflint_linux_${architecture}.zip" @@ -475,31 +492,44 @@ if [ "${TFLINT_VERSION}" != "none" ]; then else curl -sSL -o tflint_checksums.txt https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt + # Attempt GitHub Attestation verification (0.51.1+) set +e - curl -sSL -o checksums.txt.keyless.sig https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.keyless.sig + verify_tflint_attestations tflint_checksums.txt + verify_result=$? set -e - # Check that checksums.txt.keyless.sig exists and is not empty - if [ -s checksums.txt.keyless.sig ]; then - # Validate checksums with cosign - curl -sSL -o checksums.txt.pem https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.pem - ensure_cosign - cosign verify-blob \ - --certificate=/tmp/tf-downloads/checksums.txt.pem \ - --signature=/tmp/tf-downloads/checksums.txt.keyless.sig \ - --certificate-identity-regexp="^https://github.com/terraform-linters/tflint" \ - --certificate-oidc-issuer=https://token.actions.githubusercontent.com \ - /tmp/tf-downloads/tflint_checksums.txt - # Ensure that checksums.txt has $TFLINT_FILENAME - grep ${TFLINT_FILENAME} /tmp/tf-downloads/tflint_checksums.txt - # Validate downloaded file + if [ $verify_result -eq 0 ]; then sha256sum --ignore-missing -c tflint_checksums.txt + echo "(*) tflint_checksums.txt verified successfully using GitHub Attestation." else - # Fallback to older, GPG-based verification (pre-0.47.0 of tflint) - curl -sSL -o tflint_checksums.txt.sig https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.sig - curl -sSL -o tflint_key "${TFLINT_GPG_KEY_URI}" - gpg -q --import tflint_key - gpg --verify tflint_checksums.txt.sig tflint_checksums.txt + # Fallback to cosign verification + echo "(*) GitHub Attestation verification failed or not supported for this version, falling back to Cosign verification..." + set +e + curl -sSL -o checksums.txt.keyless.sig https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.keyless.sig + set -e + + # Check that checksums.txt.keyless.sig exists and is not empty + if [ -s checksums.txt.keyless.sig ]; then + # Validate checksums with cosign + curl -sSL -o checksums.txt.pem https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.pem + ensure_cosign + cosign verify-blob \ + --certificate=/tmp/tf-downloads/checksums.txt.pem \ + --signature=/tmp/tf-downloads/checksums.txt.keyless.sig \ + --certificate-identity-regexp="^https://github.com/terraform-linters/tflint" \ + --certificate-oidc-issuer=https://token.actions.githubusercontent.com \ + /tmp/tf-downloads/tflint_checksums.txt + # Ensure that checksums.txt has $TFLINT_FILENAME + grep ${TFLINT_FILENAME} /tmp/tf-downloads/tflint_checksums.txt + # Validate downloaded file + sha256sum --ignore-missing -c tflint_checksums.txt + else + # Fallback to older, GPG-based verification (pre-0.47.0 of tflint) + curl -sSL -o tflint_checksums.txt.sig https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.sig + curl -sSL -o tflint_key "${TFLINT_GPG_KEY_URI}" + gpg -q --import tflint_key + gpg --verify tflint_checksums.txt.sig tflint_checksums.txt + fi fi fi fi diff --git a/test/terraform/scenarios.json b/test/terraform/scenarios.json index 393bd3303..796efbd3e 100644 --- a/test/terraform/scenarios.json +++ b/test/terraform/scenarios.json @@ -14,7 +14,7 @@ "installSentinel": true } } - }, + }, "install_in_ubuntu_noble": { "image": "mcr.microsoft.com/devcontainers/base:noble", "features": { @@ -138,4 +138,4 @@ } } } -} \ No newline at end of file +} diff --git a/test/terraform/tflint_fallback_test.sh b/test/terraform/tflint_fallback_test.sh index 5619ff4fb..33b75f3b0 100644 --- a/test/terraform/tflint_fallback_test.sh +++ b/test/terraform/tflint_fallback_test.sh @@ -22,7 +22,6 @@ keyserver hkps://keys.openpgp.org keyserver hkps://keyserver.pgp.com" check "tflint version as installed by feature" tflint --version -check "cosign version as installed by feature" cosign version architecture="$(uname -m)" case ${architecture} in @@ -221,14 +220,31 @@ install_tflint() { curl -sSL -o /tmp/tf-downloads/${TFLINT_FILENAME} https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/${TFLINT_FILENAME} } +verify_tflint_attestations() { + local checksums=$1 + local checksums_sha256=$(sha256sum "$checksums" | cut -d " " -f 1) -try_install_dummy_tflint_cosign_version() { + check_packages jq + + curl -L -f "https://api.github.com/repos/terraform-linters/tflint/attestations/sha256:${checksums_sha256}" > attestation.json + curl_exit_code=$? + if [ $curl_exit_code -ne 0 ]; then + echo "(*) Failed to fetch GitHub Attestations for tflint checksums" + return 1 + fi + + jq ".attestations[].bundle" attestation.json > bundle.jsonl + gh at verify "$checksums" -R terraform-linters/tflint -b bundle.jsonl +} + + +try_install_dummy_tflint_version() { mode=$1 tflint_url='https://github.com/terraform-linters/tflint' mkdir -p /tmp/tf-downloads cd /tmp/tf-downloads echo -e "\nTrying to install dummy tflint version..." - TFLINT_VERSION="0.50.XYZ" + TFLINT_VERSION="0.60.XYZ" echo "Downloading tflint...v${TFLINT_VERSION}" TFLINT_FILENAME="tflint_linux_${architecture}.zip" install_tflint "$TFLINT_VERSION" @@ -237,37 +253,50 @@ try_install_dummy_tflint_cosign_version() { fi if [ "${TFLINT_SHA256}" != "dev-mode" ]; then - if [ "${TFLINT_SHA256}" != "automatic" ]; then + if [ "${TFLINT_SHA256}" != "automatic" ]; then echo "${TFLINT_SHA256} *${TFLINT_FILENAME}" > tflint_checksums.txt sha256sum --ignore-missing -c tflint_checksums.txt else curl -sSL -o tflint_checksums.txt https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt + # Attempt GitHub Attestation verification (0.51.1+) set +e - curl -sSL -o checksums.txt.keyless.sig https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.keyless.sig + verify_tflint_attestations tflint_checksums.txt + verify_result=$? set -e - - # Check that checksums.txt.keyless.sig exists and is not empty - if [ -s checksums.txt.keyless.sig ]; then - # Validate checksums with cosign - curl -sSL -o checksums.txt.pem https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.pem - ensure_cosign $mode - cosign verify-blob \ - --certificate=/tmp/tf-downloads/checksums.txt.pem \ - --signature=/tmp/tf-downloads/checksums.txt.keyless.sig \ - --certificate-identity-regexp="^https://github.com/terraform-linters/tflint" \ - --certificate-oidc-issuer=https://token.actions.githubusercontent.com \ - /tmp/tf-downloads/tflint_checksums.txt - # Ensure that checksums.txt has $TFLINT_FILENAME - grep ${TFLINT_FILENAME} /tmp/tf-downloads/tflint_checksums.txt - # Validate downloaded file + + if [ $verify_result -eq 0 ]; then sha256sum --ignore-missing -c tflint_checksums.txt + echo "(*) tflint_checksums.txt verified successfully using GitHub Attestation." else - # Fallback to older, GPG-based verification (pre-0.47.0 of tflint) - curl -sSL -o tflint_checksums.txt.sig https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.sig - curl -sSL -o tflint_key "${TFLINT_GPG_KEY_URI}" - gpg -q --import tflint_key - gpg --verify tflint_checksums.txt.sig tflint_checksums.txt + # Fallback to cosign verification + echo "(*) GitHub Attestation verification failed or not supported for this version, falling back to Cosign verification..." + set +e + curl -sSL -o checksums.txt.keyless.sig https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.keyless.sig + set -e + + # Check that checksums.txt.keyless.sig exists and is not empty + if [ -s checksums.txt.keyless.sig ]; then + # Validate checksums with cosign + curl -sSL -o checksums.txt.pem https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.pem + ensure_cosign $mode + cosign verify-blob \ + --certificate=/tmp/tf-downloads/checksums.txt.pem \ + --signature=/tmp/tf-downloads/checksums.txt.keyless.sig \ + --certificate-identity-regexp="^https://github.com/terraform-linters/tflint" \ + --certificate-oidc-issuer=https://token.actions.githubusercontent.com \ + /tmp/tf-downloads/tflint_checksums.txt + # Ensure that checksums.txt has $TFLINT_FILENAME + grep ${TFLINT_FILENAME} /tmp/tf-downloads/tflint_checksums.txt + # Validate downloaded file + sha256sum --ignore-missing -c tflint_checksums.txt + else + # Fallback to older, GPG-based verification (pre-0.47.0 of tflint) + curl -sSL -o tflint_checksums.txt.sig https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/checksums.txt.sig + curl -sSL -o tflint_key "${TFLINT_GPG_KEY_URI}" + gpg -q --import tflint_key + gpg --verify tflint_checksums.txt.sig tflint_checksums.txt + fi fi fi fi @@ -276,12 +305,10 @@ try_install_dummy_tflint_cosign_version() { sudo mv -f tflint /usr/local/bin/ } -try_install_dummy_tflint_cosign_version "mode1" +try_install_dummy_tflint_version "mode1" check "tflint version as installed when mode=1" tflint --version -check "cosign version as installed when mode=1" cosign version -try_install_dummy_tflint_cosign_version "mode2" +try_install_dummy_tflint_version "mode2" check "tflint version as installed when mode=2" tflint --version -check "cosign version as installed when mode=2" cosign version \ No newline at end of file From 73e636fb4f05942f8ebe6e9d685715d5466bf885 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:59:51 +0100 Subject: [PATCH 17/21] Support version pinning for terraform-docs in the Terraform feature (#1698) * Initial plan * Add terraformDocsVersion option to pin terraform-docs version --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- src/terraform/README.md | 1 + src/terraform/devcontainer-feature.json | 12 +++++++++++- src/terraform/install.sh | 2 +- .../install_terraform_docs_version.sh | 18 ++++++++++++++++++ test/terraform/scenarios.json | 9 +++++++++ 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 test/terraform/install_terraform_docs_version.sh diff --git a/src/terraform/README.md b/src/terraform/README.md index 4b37b4260..8cfb673dc 100644 --- a/src/terraform/README.md +++ b/src/terraform/README.md @@ -21,6 +21,7 @@ Installs the Terraform CLI and optionally TFLint and Terragrunt. Auto-detects la | installSentinel | Install sentinel, a language and framework for policy built to be embedded in existing software to enable fine-grained, logic-based policy decisions | boolean | false | | installTFsec | Install tfsec, a tool to spot potential misconfigurations for your terraform code | boolean | false | | installTerraformDocs | Install terraform-docs, a utility to generate documentation from Terraform modules | boolean | false | +| terraformDocsVersion | terraform-docs version to install (only used when installTerraformDocs is true) (https://github.com/terraform-docs/terraform-docs/releases) | string | latest | | httpProxy | Connect to a keyserver using a proxy by configuring this option | string | - | | customDownloadServer | Custom server URL for downloading Terraform and Sentinel packages, including protocol (e.g., https://releases.hashicorp.com). If not provided, the default HashiCorp download server (https://releases.hashicorp.com) will be used. | string | - | diff --git a/src/terraform/devcontainer-feature.json b/src/terraform/devcontainer-feature.json index 29d3efb30..634d865ef 100644 --- a/src/terraform/devcontainer-feature.json +++ b/src/terraform/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "terraform", - "version": "1.4.5", + "version": "1.5.0", "name": "Terraform, tflint, and TFGrunt", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/terraform", "description": "Installs the Terraform CLI and optionally TFLint and Terragrunt. Auto-detects latest version and installs needed dependencies.", @@ -50,6 +50,16 @@ "default": false, "description": "Install terraform-docs, a utility to generate documentation from Terraform modules" }, + "terraformDocsVersion": { + "type": "string", + "proposals": [ + "latest", + "0.20.0", + "0.19.0" + ], + "default": "latest", + "description": "terraform-docs version to install (only used when installTerraformDocs is true) (https://github.com/terraform-docs/terraform-docs/releases)" + }, "httpProxy": { "type": "string", "default": "", diff --git a/src/terraform/install.sh b/src/terraform/install.sh index ef0c73e5d..43779f825 100755 --- a/src/terraform/install.sh +++ b/src/terraform/install.sh @@ -18,6 +18,7 @@ TERRAGRUNT_VERSION="${TERRAGRUNT:-"latest"}" INSTALL_SENTINEL=${INSTALLSENTINEL:-false} INSTALL_TFSEC=${INSTALLTFSEC:-false} INSTALL_TERRAFORM_DOCS=${INSTALLTERRAFORMDOCS:-false} +TERRAFORM_DOCS_VERSION="${TERRAFORMDOCSVERSION:-"latest"}" CUSTOM_DOWNLOAD_SERVER="${CUSTOMDOWNLOADSERVER:-""}" # This is because ubuntu noble, ubuntu resolute and debian trixie don't support the old format of GPG keys and validation NEW_GPG_CODENAMES="trixie noble resolute" @@ -641,7 +642,6 @@ install_terraform_docs() { } if [ "${INSTALL_TERRAFORM_DOCS}" = "true" ]; then - TERRAFORM_DOCS_VERSION="latest" terraform_docs_url='https://github.com/terraform-docs/terraform-docs' find_version_from_git_tags TERRAFORM_DOCS_VERSION $terraform_docs_url tfdocs_filename="terraform-docs-v${TERRAFORM_DOCS_VERSION}-linux-${architecture}.tar.gz" diff --git a/test/terraform/install_terraform_docs_version.sh b/test/terraform/install_terraform_docs_version.sh new file mode 100644 index 000000000..4a747f82e --- /dev/null +++ b/test/terraform/install_terraform_docs_version.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e + +# Import test library for `check` command +source dev-container-features-test-lib + +# Check to make sure the user is vscode +check "user is vscode" whoami | grep vscode + +# Terraform Docs specific tests +check "terraform-docs" terraform-docs --version + +# Verify the pinned version was installed +check "terraform-docs version is pinned to 0.20.0" bash -c "terraform-docs --version | grep 'v0.20.0'" + +# Report result +reportResults diff --git a/test/terraform/scenarios.json b/test/terraform/scenarios.json index 796efbd3e..de897b5e9 100644 --- a/test/terraform/scenarios.json +++ b/test/terraform/scenarios.json @@ -79,6 +79,15 @@ } } }, + "install_terraform_docs_version": { + "image": "mcr.microsoft.com/devcontainers/base:jammy", + "features": { + "terraform": { + "installTerraformDocs": true, + "terraformDocsVersion": "0.20.0" + } + } + }, "terraform_docs_fallback_test": { "image": "mcr.microsoft.com/devcontainers/base:jammy", "features": { From c6f2fbd033181b346af0cb039ad8219cd8e0f41c Mon Sep 17 00:00:00 2001 From: Kaniska Date: Fri, 7 Aug 2026 16:31:49 +0530 Subject: [PATCH 18/21] Use GitHub App token instead of PAT in update workflows (#1702) --- .../update-aws-cli-completer-scripts.yml | 18 ++++++++++++++---- .github/workflows/update-documentation.yml | 18 ++++++++++++++---- .../workflows/update-dotnet-install-script.yml | 18 ++++++++++++++---- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/.github/workflows/update-aws-cli-completer-scripts.yml b/.github/workflows/update-aws-cli-completer-scripts.yml index fde3a29fc..ea6090fe9 100644 --- a/.github/workflows/update-aws-cli-completer-scripts.yml +++ b/.github/workflows/update-aws-cli-completer-scripts.yml @@ -9,10 +9,20 @@ jobs: runs-on: ubuntu-latest environment: documentation # grants access to secrets.PAT, for creating pull requests permissions: - contents: write - pull-requests: write + contents: read steps: - - uses: actions/checkout@v7 + - name: Generate a token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.DEVCONTAINERS_REPO_AUTOMATION_ID }} + private-key: ${{ secrets.DEVCONTAINERS_REPO_AUTOMATION_PRIVATE_KEY }} + + - name: Checkout + id: checkout + uses: actions/checkout@v7 + with: + token: ${{ steps.app-token.outputs.token }} - name: Run fetch-latest-completer-scripts.sh run: src/aws-cli/scripts/fetch-latest-completer-scripts.sh @@ -20,7 +30,7 @@ jobs: - name: Create a PR for completer scripts id: push_image_info env: - GITHUB_TOKEN: ${{ secrets.PAT }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} run: | set -e echo "Start." diff --git a/.github/workflows/update-documentation.yml b/.github/workflows/update-documentation.yml index 50a643fd7..c766a6871 100644 --- a/.github/workflows/update-documentation.yml +++ b/.github/workflows/update-documentation.yml @@ -10,11 +10,21 @@ jobs: runs-on: ubuntu-latest environment: documentation permissions: - contents: write - pull-requests: write + contents: read if: "github.ref == 'refs/heads/main'" steps: - - uses: actions/checkout@v7 + - name: Generate a token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.DEVCONTAINERS_REPO_AUTOMATION_ID }} + private-key: ${{ secrets.DEVCONTAINERS_REPO_AUTOMATION_PRIVATE_KEY }} + + - name: Checkout + id: checkout + uses: actions/checkout@v7 + with: + token: ${{ steps.app-token.outputs.token }} - name: Generate Documentation uses: devcontainers/action@v1 @@ -25,7 +35,7 @@ jobs: - name: Create a PR for Documentation id: push_image_info env: - GITHUB_TOKEN: ${{ secrets.PAT }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} run: | set -e echo "Start." diff --git a/.github/workflows/update-dotnet-install-script.yml b/.github/workflows/update-dotnet-install-script.yml index 16f737ff2..fd2161d27 100644 --- a/.github/workflows/update-dotnet-install-script.yml +++ b/.github/workflows/update-dotnet-install-script.yml @@ -9,10 +9,20 @@ jobs: runs-on: ubuntu-latest environment: documentation # grants access to secrets.PAT, for creating pull requests permissions: - contents: write - pull-requests: write + contents: read steps: - - uses: actions/checkout@v7 + - name: Generate a token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.DEVCONTAINERS_REPO_AUTOMATION_ID }} + private-key: ${{ secrets.DEVCONTAINERS_REPO_AUTOMATION_PRIVATE_KEY }} + + - name: Checkout + id: checkout + uses: actions/checkout@v7 + with: + token: ${{ steps.app-token.outputs.token }} - name: Run fetch-latest-dotnet-install.sh run: src/dotnet/scripts/fetch-latest-dotnet-install.sh @@ -20,7 +30,7 @@ jobs: - name: Create a PR for dotnet-install.sh id: push_image_info env: - GITHUB_TOKEN: ${{ secrets.PAT }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} run: | set -e echo "Start." From 8b03a989e09c8a8f11e23145ab10de101baa3e1b Mon Sep 17 00:00:00 2001 From: Tyler Kropiewnicki Date: Wed, 19 Aug 2026 09:08:14 -0400 Subject: [PATCH 19/21] feat(github-cli): support private extension installs (#1705) Co-authored-by: Kaniska --- src/github-cli/NOTES.md | 4 +++- src/github-cli/devcontainer-feature.json | 2 +- src/github-cli/install.sh | 6 +++++- src/github-cli/scripts/install-extensions.sh | 18 ++++++++++++++++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/github-cli/NOTES.md b/src/github-cli/NOTES.md index e742805e6..53c5322dd 100644 --- a/src/github-cli/NOTES.md +++ b/src/github-cli/NOTES.md @@ -6,4 +6,6 @@ This Feature should work on recent versions of Debian/Ubuntu-based distributions ## Extensions -If you set the `extensions` option, the feature will run `gh extension install` for each entry (comma-separated). Extensions are installed for the most appropriate non-root user (based on `USERNAME` / `_REMOTE_USER`), with a fallback to `root`. +If you set the `extensions` option, the feature will install each comma-separated entry. Extensions are installed for the most appropriate non-root user (based on `USERNAME` / `_REMOTE_USER`), with a fallback to `root`. + +Private extensions can be installed when `GH_TOKEN` or `GITHUB_TOKEN` is available during feature installation. The token is forwarded to the selected non-root user and used through the GitHub CLI Git credential helper. diff --git a/src/github-cli/devcontainer-feature.json b/src/github-cli/devcontainer-feature.json index 15a91e43d..58b3e5b2f 100644 --- a/src/github-cli/devcontainer-feature.json +++ b/src/github-cli/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "github-cli", - "version": "1.1.0", + "version": "1.1.1", "name": "GitHub CLI", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/github-cli", "description": "Installs the GitHub CLI. Auto-detects latest version and installs needed dependencies.", diff --git a/src/github-cli/install.sh b/src/github-cli/install.sh index e3eaba0c3..3638d9392 100755 --- a/src/github-cli/install.sh +++ b/src/github-cli/install.sh @@ -271,7 +271,11 @@ if [ -n "${EXTENSIONS}" ]; then else EXTENSIONS_ESCAPED="$(printf '%q' "${EXTENSIONS}")" USERNAME_ESCAPED="$(printf '%q' "${USERNAME}")" - su - "${USERNAME}" -c "EXTENSIONS=${EXTENSIONS_ESCAPED} USERNAME=${USERNAME_ESCAPED} INSTALL_EXTENSIONS=true bash '${EXTENSIONS_SCRIPT}'" + su \ + --login \ + --whitelist-environment=GH_TOKEN,GITHUB_TOKEN \ + --command "EXTENSIONS=${EXTENSIONS_ESCAPED} USERNAME=${USERNAME_ESCAPED} INSTALL_EXTENSIONS=true bash '${EXTENSIONS_SCRIPT}'" \ + "${USERNAME}" INSTALL_EXTENSIONS=false bash "${EXTENSIONS_SCRIPT}" fi fi diff --git a/src/github-cli/scripts/install-extensions.sh b/src/github-cli/scripts/install-extensions.sh index 436accf03..f8a893534 100644 --- a/src/github-cli/scripts/install-extensions.sh +++ b/src/github-cli/scripts/install-extensions.sh @@ -26,18 +26,32 @@ install_extension() { mkdir -p "${extensions_root}" if [ ! -d "${extensions_root}/${repo_name}" ]; then - git clone --depth 1 "https://github.com/${extension}.git" "${extensions_root}/${repo_name}" + git \ + -c credential.helper= \ + -c credential.helper='!gh auth git-credential' \ + clone --depth 1 "https://github.com/${extension}.git" "${extensions_root}/${repo_name}" fi } ensure_gh_extension_list_wrapper() { + local gh_config_dir + if [ "$(id -u)" -ne 0 ]; then return fi - if gh extension list >/dev/null 2>&1; then + gh_config_dir="$(mktemp -d)" + if env \ + -u GH_TOKEN \ + -u GITHUB_TOKEN \ + -u GH_ENTERPRISE_TOKEN \ + -u GITHUB_ENTERPRISE_TOKEN \ + GH_CONFIG_DIR="${gh_config_dir}" \ + gh extension list >/dev/null 2>&1; then + rm -rf "${gh_config_dir}" return fi + rm -rf "${gh_config_dir}" cat > /usr/local/bin/gh <<'EOF' #!/usr/bin/env bash From be75f4afe80de657343c8c4966e32e3e96935c93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 12:21:35 +0100 Subject: [PATCH 20/21] Bump actions/create-github-app-token from 2 to 3 (#1706) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2 to 3. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Changelog](https://github.com/actions/create-github-app-token/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/create-github-app-token/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/update-aws-cli-completer-scripts.yml | 2 +- .github/workflows/update-documentation.yml | 2 +- .github/workflows/update-dotnet-install-script.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/update-aws-cli-completer-scripts.yml b/.github/workflows/update-aws-cli-completer-scripts.yml index ea6090fe9..eeec43475 100644 --- a/.github/workflows/update-aws-cli-completer-scripts.yml +++ b/.github/workflows/update-aws-cli-completer-scripts.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Generate a token id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ vars.DEVCONTAINERS_REPO_AUTOMATION_ID }} private-key: ${{ secrets.DEVCONTAINERS_REPO_AUTOMATION_PRIVATE_KEY }} diff --git a/.github/workflows/update-documentation.yml b/.github/workflows/update-documentation.yml index c766a6871..e5023ccb3 100644 --- a/.github/workflows/update-documentation.yml +++ b/.github/workflows/update-documentation.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Generate a token id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ vars.DEVCONTAINERS_REPO_AUTOMATION_ID }} private-key: ${{ secrets.DEVCONTAINERS_REPO_AUTOMATION_PRIVATE_KEY }} diff --git a/.github/workflows/update-dotnet-install-script.yml b/.github/workflows/update-dotnet-install-script.yml index fd2161d27..d8f532d9d 100644 --- a/.github/workflows/update-dotnet-install-script.yml +++ b/.github/workflows/update-dotnet-install-script.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Generate a token id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ vars.DEVCONTAINERS_REPO_AUTOMATION_ID }} private-key: ${{ secrets.DEVCONTAINERS_REPO_AUTOMATION_PRIVATE_KEY }} From 23c5205d67278aa170e1bc70a115ad0d89dd584b Mon Sep 17 00:00:00 2001 From: Kaniska Date: Fri, 21 Aug 2026 16:51:53 +0530 Subject: [PATCH 21/21] Removing flaky tests which are not compatible configurations and not required henceforth (#1701) Removing flaky tests which are not compatible cases --- .github/workflows/test-all.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/test-all.yaml b/.github/workflows/test-all.yaml index dd1d216a0..3d4aa3fed 100644 --- a/.github/workflows/test-all.yaml +++ b/.github/workflows/test-all.yaml @@ -50,6 +50,19 @@ jobs: "mcr.microsoft.com/devcontainers/base:debian", "mcr.microsoft.com/devcontainers/base:noble" ] + exclude: + - features: oryx + baseImage: ubuntu:jammy + - features: oryx + baseImage: mcr.microsoft.com/devcontainers/base:ubuntu + - features: docker-in-docker + baseImage: mcr.microsoft.com/devcontainers/base:debian + - features: docker-outside-of-docker + baseImage: mcr.microsoft.com/devcontainers/base:debian + - features: docker-in-docker + baseImage: mcr.microsoft.com/devcontainers/base:ubuntu + - features: docker-outside-of-docker + baseImage: mcr.microsoft.com/devcontainers/base:ubuntu steps: - uses: actions/checkout@v7 @@ -100,6 +113,15 @@ jobs: - name: "Install latest devcontainer CLI" run: npm install -g @devcontainers/cli + - name: "Exclude iptables-isolation scenarios from docker-in-docker (run in separate 'iptables-isolation' job)" + if: matrix.features == 'docker-in-docker' + run: | + sudo apt-get update && sudo apt-get install -y jq + sed 's://.*$::' test/docker-in-docker/scenarios.json \ + | jq 'del(.docker_with_default_iptables, .docker_with_default_iptables_ubuntu)' \ + > test/docker-in-docker/scenarios.json.tmp + mv test/docker-in-docker/scenarios.json.tmp test/docker-in-docker/scenarios.json + - name: "Testing '${{ matrix.features }}' scenarios" run: devcontainer features test -f ${{ matrix.features }} --skip-autogenerated .