From d2b4c7e15e3699a694786a8809924e20ea711500 Mon Sep 17 00:00:00 2001 From: Allison Thackston <73732028+althack@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:12:13 -0700 Subject: [PATCH 1/4] Add ROS 2 development container feature --- .devcontainer/prepare-local-features.sh | 3 +- .../ros2-feature-smoke/devcontainer.json | 8 ++ .vscode/tasks.json | 65 +++++++++ README.md | 3 +- features/src/ros2/NOTES.md | 32 +++++ features/src/ros2/README.md | 58 ++++++++ features/src/ros2/devcontainer-feature.json | 39 ++++++ features/src/ros2/distributions.json | 26 ++++ features/src/ros2/install.sh | 128 ++++++++++++++++++ features/test/ros2/humble.sh | 11 ++ features/test/ros2/kilted.sh | 11 ++ features/test/ros2/lyrical.sh | 11 ++ features/test/ros2/scenarios.json | 22 +++ features/test/ros2/test.sh | 16 +++ features/test/test_all.sh | 6 +- features/test/validate_ros2_config.py | 51 +++++++ 16 files changed, 486 insertions(+), 4 deletions(-) create mode 100644 .devcontainer/ros2-feature-smoke/devcontainer.json create mode 100644 features/src/ros2/NOTES.md create mode 100644 features/src/ros2/README.md create mode 100644 features/src/ros2/devcontainer-feature.json create mode 100644 features/src/ros2/distributions.json create mode 100755 features/src/ros2/install.sh create mode 100755 features/test/ros2/humble.sh create mode 100755 features/test/ros2/kilted.sh create mode 100755 features/test/ros2/lyrical.sh create mode 100644 features/test/ros2/scenarios.json create mode 100755 features/test/ros2/test.sh create mode 100755 features/test/validate_ros2_config.py diff --git a/.devcontainer/prepare-local-features.sh b/.devcontainer/prepare-local-features.sh index e1ed0c0..f7142bf 100755 --- a/.devcontainer/prepare-local-features.sh +++ b/.devcontainer/prepare-local-features.sh @@ -6,7 +6,8 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" local_features_dir="${repo_root}/.devcontainer/local-features" mkdir -p "${local_features_dir}" -rm -rf "${local_features_dir}/linux-x11-forwarding" "${local_features_dir}/x11" "${local_features_dir}/wayland" +rm -rf "${local_features_dir}/linux-x11-forwarding" "${local_features_dir}/ros2" "${local_features_dir}/x11" "${local_features_dir}/wayland" cp -R "${repo_root}/features/src/linux-x11-forwarding" "${local_features_dir}/linux-x11-forwarding" +cp -R "${repo_root}/features/src/ros2" "${local_features_dir}/ros2" echo "Prepared local smoke-test features in ${local_features_dir}" diff --git a/.devcontainer/ros2-feature-smoke/devcontainer.json b/.devcontainer/ros2-feature-smoke/devcontainer.json new file mode 100644 index 0000000..52bb264 --- /dev/null +++ b/.devcontainer/ros2-feature-smoke/devcontainer.json @@ -0,0 +1,8 @@ +{ + "name": "ros2-feature-smoke", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "../local-features/ros2": {} + }, + "postCreateCommand": "bash -lc 'ros2 pkg prefix rviz2 >/dev/null && command -v colcon >/dev/null && command -v rosdep >/dev/null && command -v vcs >/dev/null'" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bb51188..5b8401c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -41,6 +41,71 @@ ], "problemMatcher": [] }, + { + "label": "Prepare Local Features", + "type": "process", + "command": "bash", + "args": [ + ".devcontainer/prepare-local-features.sh" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, + { + "label": "Local Smoke Test ROS 2 Feature", + "dependsOrder": "sequence", + "dependsOn": [ + "Prepare Local Features" + ], + "type": "process", + "command": "devcontainer", + "args": [ + "up", + "--workspace-folder", + "${workspaceFolder}", + "--config", + "${workspaceFolder}/.devcontainer/ros2-feature-smoke/devcontainer.json" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, + { + "label": "Test ROS 2 Feature", + "type": "process", + "command": "devcontainer", + "args": [ + "features", + "test", + "--features", + "ros2", + "--project-folder", + "./features" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, + { + "label": "Test All Features", + "type": "process", + "command": "bash", + "args": [ + "features/test/test_all.sh" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": [] + }, { "label": "Generate Feature Docs", "type": "process", diff --git a/README.md b/README.md index ff5e7c3..cb70f11 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,11 @@ Devcontainer templates repository focused on robotics and simulation. - `.devcontainer/gz-smoke/devcontainer.json` - `.devcontainer/linux-x11-forwarding-smoke/devcontainer.json` +- `.devcontainer/ros2-feature-smoke/devcontainer.json` Before using the samples locally, run `bash .devcontainer/prepare-local-features.sh` so `devcontainer up` can resolve -the unpublished Feature. +the unpublished Features. On classic Xorg hosts where the authority cookie lives outside `XDG_RUNTIME_DIR`, add a bind mount to `/tmp/devcontainer-xauthority-host`. diff --git a/features/src/ros2/NOTES.md b/features/src/ros2/NOTES.md new file mode 100644 index 0000000..67fefc0 --- /dev/null +++ b/features/src/ros2/NOTES.md @@ -0,0 +1,32 @@ +## Supported Ubuntu releases + +ROS 2 deb packages target a specific Ubuntu release. Choose a compatible base +image for the selected `distro`. The default `auto` setting selects: + +| ROS 2 distribution | Ubuntu release | +|---|---| +| Humble | 22.04 (Jammy) | +| Jazzy | 24.04 (Noble) | +| Kilted | 24.04 (Noble) | +| Lyrical | 26.04 (Resolute) | + +The installation stops with an explanatory error when the selected ROS 2 and +Ubuntu releases are incompatible. On Ubuntu 24.04, `auto` selects Jazzy because +it is the long-term ROS 2 release; select Kilted explicitly if desired. + +The `package` option defaults to the GUI-oriented `desktop` installation. Select +`ros-base` for a smaller installation. In both cases, the Feature installs +`ros-dev-tools`, including common ROS development commands such as `colcon`, +`rosdep`, and `vcs`. + +## Shell and workspace setup + +The ROS installation is sourced automatically for the existing container user. +If the configured `workspace` contains `install/setup.sh`, that overlay is +sourced afterward. The Feature does not create or replace the container user. + +ROS 2 packages installed from debs use Ubuntu's system Python. Conda or a +separately installed Python interpreter may be incompatible with those packages. + +The Feature's ROS 2-to-Ubuntu compatibility data is stored in +`distributions.json` and packaged with the Feature. diff --git a/features/src/ros2/README.md b/features/src/ros2/README.md new file mode 100644 index 0000000..9c274df --- /dev/null +++ b/features/src/ros2/README.md @@ -0,0 +1,58 @@ + +# ROS 2 (ros2) + +Install ROS 2 and the tools needed to develop ROS packages. + +## Example Usage + +```json +"features": { + "ghcr.io/althack/devcontainers/ros2:0": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| distro | ROS 2 distribution to install. By default, select the recommended distribution for the container's Ubuntu release. | string | auto | +| package | ROS 2 metapackage to install. | string | desktop | +| workspace | Workspace whose install/setup.sh file should be sourced when present. | string | ${containerWorkspaceFolder} | + +## Supported Ubuntu releases + +ROS 2 deb packages target a specific Ubuntu release. Choose a compatible base +image for the selected `distro`. The default `auto` setting selects: + +| ROS 2 distribution | Ubuntu release | +|---|---| +| Humble | 22.04 (Jammy) | +| Jazzy | 24.04 (Noble) | +| Kilted | 24.04 (Noble) | +| Lyrical | 26.04 (Resolute) | + +The installation stops with an explanatory error when the selected ROS 2 and +Ubuntu releases are incompatible. On Ubuntu 24.04, `auto` selects Jazzy because +it is the long-term ROS 2 release; select Kilted explicitly if desired. + +The `package` option defaults to the GUI-oriented `desktop` installation. Select +`ros-base` for a smaller installation. In both cases, the Feature installs +`ros-dev-tools`, including common ROS development commands such as `colcon`, +`rosdep`, and `vcs`. + +## Shell and workspace setup + +The ROS installation is sourced automatically for the existing container user. +If the configured `workspace` contains `install/setup.sh`, that overlay is +sourced afterward. The Feature does not create or replace the container user. + +ROS 2 packages installed from debs use Ubuntu's system Python. Conda or a +separately installed Python interpreter may be incompatible with those packages. + +The Feature's ROS 2-to-Ubuntu compatibility data is stored in +`distributions.json` and packaged with the Feature. + + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/althack/devcontainers/blob/main/features/src/ros2/devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/features/src/ros2/devcontainer-feature.json b/features/src/ros2/devcontainer-feature.json new file mode 100644 index 0000000..9b7ab44 --- /dev/null +++ b/features/src/ros2/devcontainer-feature.json @@ -0,0 +1,39 @@ +{ + "id": "ros2", + "version": "0.0.0", + "name": "ROS 2", + "description": "Install ROS 2 and the tools needed to develop ROS packages.", + "documentationURL": "https://github.com/althack/devcontainers/tree/main/features/src/ros2", + "licenseURL": "https://github.com/althack/devcontainers/blob/main/LICENSE", + "options": { + "distro": { + "type": "string", + "proposals": [ + "auto", + "humble", + "jazzy", + "kilted", + "lyrical" + ], + "default": "auto", + "description": "ROS 2 distribution to install. By default, select the recommended distribution for the container's Ubuntu release." + }, + "package": { + "type": "string", + "proposals": [ + "ros-base", + "desktop" + ], + "default": "desktop", + "description": "ROS 2 metapackage to install." + }, + "workspace": { + "type": "string", + "default": "${containerWorkspaceFolder}", + "description": "Workspace whose install/setup.sh file should be sourced when present." + } + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/common-utils" + ] +} diff --git a/features/src/ros2/distributions.json b/features/src/ros2/distributions.json new file mode 100644 index 0000000..62bfaae --- /dev/null +++ b/features/src/ros2/distributions.json @@ -0,0 +1,26 @@ +{ + "ubuntuReleases": { + "jammy": { + "version": "22.04", + "defaultDistro": "humble", + "supportedDistros": [ + "humble" + ] + }, + "noble": { + "version": "24.04", + "defaultDistro": "jazzy", + "supportedDistros": [ + "jazzy", + "kilted" + ] + }, + "resolute": { + "version": "26.04", + "defaultDistro": "lyrical", + "supportedDistros": [ + "lyrical" + ] + } + } +} diff --git a/features/src/ros2/install.sh b/features/src/ros2/install.sh new file mode 100755 index 0000000..2369370 --- /dev/null +++ b/features/src/ros2/install.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ros_distro="${DISTRO:-auto}" +ros_package="${PACKAGE:-desktop}" +ros_workspace="${WORKSPACE:-${_REMOTE_USER_HOME:-/workspaces}/ros2_ws}" +feature_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +distribution_config="${feature_dir}/distributions.json" + +if [[ "$(id -u)" -ne 0 ]]; then + echo "The ROS 2 Feature must run as root." >&2 + exit 1 +fi + +if [[ ! -r /etc/os-release ]]; then + echo "Unable to determine the base operating system." >&2 + exit 1 +fi + +# shellcheck disable=SC1091 +source /etc/os-release + +if [[ "${ID:-}" != "ubuntu" ]]; then + echo "ROS 2 deb packages are supported only on Ubuntu; found ${ID:-unknown}." >&2 + exit 1 +fi + +case "${ros_package}" in + ros-base | desktop) + ;; + *) + echo "Unsupported ROS 2 package: ${ros_package}." >&2 + exit 1 + ;; +esac + +export DEBIAN_FRONTEND=noninteractive + +apt-get update +apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + jq \ + locales \ + software-properties-common + +ubuntu_codename="${UBUNTU_CODENAME:-${VERSION_CODENAME:-}}" +ubuntu_release="$(jq -c --arg codename "${ubuntu_codename}" '.ubuntuReleases[$codename] // empty' "${distribution_config}")" + +if [[ -z "${ubuntu_release}" ]]; then + echo "Ubuntu ${VERSION_ID:-unknown} (${ubuntu_codename:-unknown}) is not supported by this ROS 2 Feature." >&2 + exit 1 +fi + +ubuntu_version="$(jq -r '.version' <<<"${ubuntu_release}")" + +if [[ "${ros_distro}" == "auto" ]]; then + ros_distro="$(jq -r '.defaultDistro' <<<"${ubuntu_release}")" + echo "Selected ROS 2 ${ros_distro} for Ubuntu ${VERSION_ID:-unknown} (${ubuntu_codename})." +fi + +if ! jq -e --arg distro "${ros_distro}" '.supportedDistros | index($distro) != null' <<<"${ubuntu_release}" >/dev/null; then + supported_distros="$(jq -r '.supportedDistros | join(", ")' <<<"${ubuntu_release}")" + echo "ROS 2 ${ros_distro} is not supported on Ubuntu ${ubuntu_version} (${ubuntu_codename}); select one of: ${supported_distros}." >&2 + exit 1 +fi + +locale-gen en_US en_US.UTF-8 +update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 +add-apt-repository -y universe + +ros_apt_source_release_url="$( + curl --fail --silent --show-error --location \ + --output /dev/null \ + --write-out '%{url_effective}' \ + https://github.com/ros-infrastructure/ros-apt-source/releases/latest +)" +ros_apt_source_version="${ros_apt_source_release_url##*/}" + +if [[ ! "${ros_apt_source_version}" =~ ^[0-9]+(\.[0-9]+)+$ ]]; then + echo "Unable to determine the latest ros2-apt-source release." >&2 + exit 1 +fi + +ros_apt_source_package="/tmp/ros2-apt-source.deb" +curl --fail --silent --show-error --location \ + "https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ros_apt_source_version}/ros2-apt-source_${ros_apt_source_version}.${ubuntu_codename}_all.deb" \ + --output "${ros_apt_source_package}" +dpkg -i "${ros_apt_source_package}" +rm -f "${ros_apt_source_package}" + +apt-get update +apt-get install -y --no-install-recommends \ + "ros-${ros_distro}-${ros_package}" \ + python3-argcomplete \ + ros-dev-tools + +if [[ ! -e /etc/ros/rosdep/sources.list.d/20-default.list ]]; then + rosdep init +fi + +cat > /etc/profile.d/ros2.sh <> "${shell_init}" + fi +done + +rm -rf /var/lib/apt/lists/* + +echo "Installed ROS 2 ${ros_distro} (${ros_package}) with ROS development tools." diff --git a/features/test/ros2/humble.sh b/features/test/ros2/humble.sh new file mode 100755 index 0000000..d1f3ee6 --- /dev/null +++ b/features/test/ros2/humble.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "ROS 2 Humble is selected" bash -lc '[ "${ROS_DISTRO:-}" = "humble" ]' +check "ROS 2 CLI is installed" bash -lc 'ros2 pkg prefix rclcpp >/dev/null' +check "development tools are installed" bash -lc 'command -v colcon >/dev/null && command -v rosdep >/dev/null && command -v vcs >/dev/null' + +reportResults diff --git a/features/test/ros2/kilted.sh b/features/test/ros2/kilted.sh new file mode 100755 index 0000000..9d7ef04 --- /dev/null +++ b/features/test/ros2/kilted.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "ROS 2 Kilted is selected" bash -lc '[ "${ROS_DISTRO:-}" = "kilted" ]' +check "ROS 2 CLI is installed" bash -lc 'ros2 pkg prefix rclcpp >/dev/null' +check "development tools are installed" bash -lc 'command -v colcon >/dev/null && command -v rosdep >/dev/null && command -v vcs >/dev/null' + +reportResults diff --git a/features/test/ros2/lyrical.sh b/features/test/ros2/lyrical.sh new file mode 100755 index 0000000..37a1171 --- /dev/null +++ b/features/test/ros2/lyrical.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "ROS 2 Lyrical is selected" bash -lc '[ "${ROS_DISTRO:-}" = "lyrical" ]' +check "ROS 2 CLI is installed" bash -lc 'ros2 pkg prefix rclcpp >/dev/null' +check "development tools are installed" bash -lc 'command -v colcon >/dev/null && command -v rosdep >/dev/null && command -v vcs >/dev/null' + +reportResults diff --git a/features/test/ros2/scenarios.json b/features/test/ros2/scenarios.json new file mode 100644 index 0000000..ee6984e --- /dev/null +++ b/features/test/ros2/scenarios.json @@ -0,0 +1,22 @@ +{ + "humble": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04", + "features": { + "ros2": {} + } + }, + "lyrical": { + "image": "ubuntu:26.04", + "features": { + "ros2": {} + } + }, + "kilted": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "ros2": { + "distro": "kilted" + } + } + } +} diff --git a/features/test/ros2/test.sh b/features/test/ros2/test.sh new file mode 100755 index 0000000..24cae20 --- /dev/null +++ b/features/test/ros2/test.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "ROS 2 automatically selects Jazzy on Noble" bash -lc '[ "${ROS_DISTRO:-}" = "jazzy" ]' +check "ROS 2 CLI is installed" bash -lc 'ros2 pkg prefix rclcpp >/dev/null' +check "ROS 2 desktop is installed by default" bash -lc 'ros2 pkg prefix rviz2 >/dev/null' +check "colcon is installed" bash -lc 'command -v colcon >/dev/null' +check "rosdep is installed" bash -lc 'command -v rosdep >/dev/null' +check "vcs is installed" bash -lc 'command -v vcs >/dev/null' +check "rosdep is initialized" test -f /etc/ros/rosdep/sources.list.d/20-default.list +check "ROS profile is installed" test -f /etc/profile.d/ros2.sh + +reportResults diff --git a/features/test/test_all.sh b/features/test/test_all.sh index 45f3eeb..f7331e1 100755 --- a/features/test/test_all.sh +++ b/features/test/test_all.sh @@ -2,13 +2,15 @@ set -euo pipefail features_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -base_image="${DEVCONTAINER_BASE_IMAGE:-mcr.microsoft.com/devcontainers/base:ubuntu}" +base_image="${DEVCONTAINER_BASE_IMAGE:-mcr.microsoft.com/devcontainers/base:ubuntu-24.04}" if ! command -v devcontainer >/dev/null 2>&1; then echo "devcontainer CLI is required. Install it with 'npm install -g @devcontainers/cli'." >&2 exit 1 fi +python3 "${features_root}/test/validate_ros2_config.py" + cleanup_paths=() cleanup() { @@ -40,7 +42,7 @@ touch "${XAUTHORITY}" touch "${xwayland_xauthority}" touch "${XDG_RUNTIME_DIR}/pulse-native" -for feature in linux-x11-forwarding; do +for feature in linux-x11-forwarding ros2; do devcontainer features test \ --base-image "${base_image}" \ --features "${feature}" \ diff --git a/features/test/validate_ros2_config.py b/features/test/validate_ros2_config.py new file mode 100755 index 0000000..cd1fcaf --- /dev/null +++ b/features/test/validate_ros2_config.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 + +import json +from pathlib import Path +import re + + +feature_root = Path(__file__).resolve().parents[1] / "src" / "ros2" + +with (feature_root / "distributions.json").open(encoding="utf-8") as stream: + config = json.load(stream) + +with (feature_root / "devcontainer-feature.json").open(encoding="utf-8") as stream: + manifest = json.load(stream) + +ubuntu_releases = config["ubuntuReleases"] +proposals = manifest["options"]["distro"]["proposals"] +default = manifest["options"]["distro"]["default"] + +if set(config) != {"ubuntuReleases"}: + raise SystemExit("distributions.json must contain only ubuntuReleases") + +distributions = [] +for codename, settings in ubuntu_releases.items(): + expected_fields = {"version", "defaultDistro", "supportedDistros"} + if set(settings) != expected_fields: + raise SystemExit( + f"{codename} must define exactly: {', '.join(sorted(expected_fields))}" + ) + if not re.fullmatch(r"[a-z]+", codename): + raise SystemExit(f"{codename} is not a valid Ubuntu codename") + if not re.fullmatch(r"[0-9]{2}\.[0-9]{2}", settings["version"]): + raise SystemExit(f"{codename} has an invalid Ubuntu version") + if not settings["supportedDistros"]: + raise SystemExit(f"{codename} must support at least one ROS 2 distribution") + if settings["defaultDistro"] not in settings["supportedDistros"]: + raise SystemExit(f"{codename}'s default must be one of its supported distros") + for name in settings["supportedDistros"]: + if name in distributions: + raise SystemExit(f"ROS 2 {name} is listed for more than one Ubuntu release") + distributions.append(name) + +if proposals != ["auto", *distributions]: + raise SystemExit( + "ROS 2 distro proposals must be auto followed by distributions.json entries" + ) + +if default != "auto": + raise SystemExit("The ROS 2 distro option must default to auto") + +print(f"Validated {len(distributions)} ROS 2 distribution definitions") From 88c8b7a636d1623e10f027c183cf22fb67802a25 Mon Sep 17 00:00:00 2001 From: Allison Thackston <73732028+althack@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:04:15 -0700 Subject: [PATCH 2/4] Expand feature testing workflows --- .github/workflows/test-main.yaml | 35 +++++++ .github/workflows/test-pr.yaml | 28 +++++- .vscode/tasks.json | 99 +++++++++++++++++-- .../test/linux-x11-forwarding/prepare-host.sh | 14 +++ features/test/ros2/scenarios.json | 11 ++- .../validate_config.py} | 4 +- features/test/test_all.sh | 50 ---------- 7 files changed, 176 insertions(+), 65 deletions(-) create mode 100644 .github/workflows/test-main.yaml create mode 100755 features/test/linux-x11-forwarding/prepare-host.sh rename features/test/{validate_ros2_config.py => ros2/validate_config.py} (93%) mode change 100755 => 100644 delete mode 100755 features/test/test_all.sh diff --git a/.github/workflows/test-main.yaml b/.github/workflows/test-main.yaml new file mode 100644 index 0000000..2ccb739 --- /dev/null +++ b/.github/workflows/test-main.yaml @@ -0,0 +1,35 @@ +name: "CI - Test ROS 2 Distribution Matrix" + +on: + push: + # branches: + # - main + workflow_dispatch: + +jobs: + ros2-matrix: + name: "ROS 2 ${{ matrix.scenario }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + scenario: + - humble + - kilted + - lyrical + steps: + - uses: actions/checkout@v6 + + - name: Install devcontainer CLI + run: npm install --global @devcontainers/cli@0.88.0 + + - name: Validate ROS 2 configuration + run: python3 features/test/ros2/validate_config.py + + - name: Test ROS 2 ${{ matrix.scenario }} + run: | + devcontainer features test \ + --features ros2 \ + --project-folder features \ + --skip-autogenerated \ + --filter "${{ matrix.scenario }}" diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index 18a8a00..dfd00a0 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -20,16 +20,38 @@ jobs: feature-tests: runs-on: ubuntu-latest + env: + DISPLAY: ":99" + XDG_RUNTIME_DIR: /tmp/devcontainers-x11-runtime + XAUTHORITY: /tmp/devcontainers-xauthority.test + PULSE_SERVER: unix:/tmp/devcontainers-x11-runtime/pulse-native steps: - uses: actions/checkout@v6 - name: Install devcontainer CLI run: npm install --global @devcontainers/cli@0.88.0 - - name: Validate feature metadata - run: bash features/test/test_all.sh + - name: Prepare X11 test environment + run: bash features/test/linux-x11-forwarding/prepare-host.sh - - name: Validate Feature publishing metadata + - name: Validate ROS 2 configuration + run: python3 features/test/ros2/validate_config.py + + - name: Test every Feature with default options + run: | + devcontainer features test \ + --base-image mcr.microsoft.com/devcontainers/base:ubuntu-24.04 \ + --project-folder features \ + --skip-scenarios + + - name: Test X11 option scenarios + run: | + devcontainer features test \ + --features linux-x11-forwarding \ + --project-folder features \ + --skip-autogenerated + + - name: Package Features run: | devcontainer features package features/src \ --output-folder "${RUNNER_TEMP}/features" \ diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5b8401c..0272182 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -74,32 +74,117 @@ "problemMatcher": [] }, { - "label": "Test ROS 2 Feature", + "label": "Validate ROS 2 Configuration", + "type": "process", + "command": "python3", + "args": [ + "features/test/ros2/validate_config.py" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, + { + "label": "Prepare X11 Test Environment", + "type": "process", + "command": "bash", + "args": [ + "features/test/linux-x11-forwarding/prepare-host.sh" + ], + "options": { + "cwd": "${workspaceFolder}", + "env": { + "DISPLAY": ":99", + "XDG_RUNTIME_DIR": "/tmp/devcontainers-x11-runtime", + "XAUTHORITY": "/tmp/devcontainers-xauthority.test", + "PULSE_SERVER": "unix:/tmp/devcontainers-x11-runtime/pulse-native" + } + }, + "problemMatcher": [] + }, + { + "label": "Test Feature Defaults", + "dependsOrder": "sequence", + "dependsOn": [ + "Prepare X11 Test Environment" + ], + "type": "process", + "command": "devcontainer", + "args": [ + "features", + "test", + "--base-image", + "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "--project-folder", + "./features", + "--skip-scenarios" + ], + "options": { + "cwd": "${workspaceFolder}", + "env": { + "DISPLAY": ":99", + "XDG_RUNTIME_DIR": "/tmp/devcontainers-x11-runtime", + "XAUTHORITY": "/tmp/devcontainers-xauthority.test", + "PULSE_SERVER": "unix:/tmp/devcontainers-x11-runtime/pulse-native" + } + }, + "problemMatcher": [] + }, + { + "label": "Test X11 Scenarios", + "dependsOrder": "sequence", + "dependsOn": [ + "Prepare X11 Test Environment" + ], "type": "process", "command": "devcontainer", "args": [ "features", "test", "--features", - "ros2", + "linux-x11-forwarding", "--project-folder", - "./features" + "./features", + "--skip-autogenerated" ], "options": { - "cwd": "${workspaceFolder}" + "cwd": "${workspaceFolder}", + "env": { + "DISPLAY": ":99", + "XDG_RUNTIME_DIR": "/tmp/devcontainers-x11-runtime", + "XAUTHORITY": "/tmp/devcontainers-xauthority.test", + "PULSE_SERVER": "unix:/tmp/devcontainers-x11-runtime/pulse-native" + } }, "problemMatcher": [] }, { - "label": "Test All Features", + "label": "Test ROS 2 Compatibility Matrix", "type": "process", - "command": "bash", + "command": "devcontainer", "args": [ - "features/test/test_all.sh" + "features", + "test", + "--features", + "ros2", + "--project-folder", + "./features", + "--skip-autogenerated" ], "options": { "cwd": "${workspaceFolder}" }, + "problemMatcher": [] + }, + { + "label": "Test PR Feature Suite", + "dependsOrder": "sequence", + "dependsOn": [ + "Validate ROS 2 Configuration", + "Test Feature Defaults", + "Test X11 Scenarios" + ], "group": { "kind": "test", "isDefault": true diff --git a/features/test/linux-x11-forwarding/prepare-host.sh b/features/test/linux-x11-forwarding/prepare-host.sh new file mode 100755 index 0000000..1c303c9 --- /dev/null +++ b/features/test/linux-x11-forwarding/prepare-host.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -euo pipefail + +x11_runtime_dir="${XDG_RUNTIME_DIR:-/tmp/devcontainers-x11-runtime}" +xauthority="${XAUTHORITY:-/tmp/devcontainers-xauthority.test}" +xwayland_xauthority="${FEATURE_TEST_XWAYLAND_XAUTHORITY:-${x11_runtime_dir}/.mutter-Xwaylandauth.test}" +pulse_socket="${FEATURE_TEST_PULSE_SOCKET:-${x11_runtime_dir}/pulse-native}" + +mkdir -p /tmp/.X11-unix "$(dirname "${xauthority}")" "${x11_runtime_dir}" +chmod 700 "${x11_runtime_dir}" +touch "${xauthority}" "${xwayland_xauthority}" "${pulse_socket}" + +echo "Prepared X11 host fixtures." diff --git a/features/test/ros2/scenarios.json b/features/test/ros2/scenarios.json index ee6984e..cd82702 100644 --- a/features/test/ros2/scenarios.json +++ b/features/test/ros2/scenarios.json @@ -2,20 +2,25 @@ "humble": { "image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04", "features": { - "ros2": {} + "ros2": { + "package": "ros-base" + } } }, "lyrical": { "image": "ubuntu:26.04", "features": { - "ros2": {} + "ros2": { + "package": "ros-base" + } } }, "kilted": { "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", "features": { "ros2": { - "distro": "kilted" + "distro": "kilted", + "package": "ros-base" } } } diff --git a/features/test/validate_ros2_config.py b/features/test/ros2/validate_config.py old mode 100755 new mode 100644 similarity index 93% rename from features/test/validate_ros2_config.py rename to features/test/ros2/validate_config.py index cd1fcaf..270796f --- a/features/test/validate_ros2_config.py +++ b/features/test/ros2/validate_config.py @@ -5,7 +5,7 @@ import re -feature_root = Path(__file__).resolve().parents[1] / "src" / "ros2" +feature_root = Path(__file__).resolve().parents[2] / "src" / "ros2" with (feature_root / "distributions.json").open(encoding="utf-8") as stream: config = json.load(stream) @@ -46,6 +46,6 @@ ) if default != "auto": - raise SystemExit("The ROS 2 distro option must default to auto") + raise SystemExit("The default ROS 2 distro option must be auto") print(f"Validated {len(distributions)} ROS 2 distribution definitions") diff --git a/features/test/test_all.sh b/features/test/test_all.sh deleted file mode 100755 index f7331e1..0000000 --- a/features/test/test_all.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -features_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -base_image="${DEVCONTAINER_BASE_IMAGE:-mcr.microsoft.com/devcontainers/base:ubuntu-24.04}" - -if ! command -v devcontainer >/dev/null 2>&1; then - echo "devcontainer CLI is required. Install it with 'npm install -g @devcontainers/cli'." >&2 - exit 1 -fi - -python3 "${features_root}/test/validate_ros2_config.py" - -cleanup_paths=() - -cleanup() { - for path in "${cleanup_paths[@]}"; do - if [[ -d "${path}" ]]; then - rm -rf "${path}" - elif [[ -f "${path}" ]]; then - rm -f "${path}" - fi - done -} - -trap cleanup EXIT - -mkdir -p /tmp/.X11-unix - -export DISPLAY="${FEATURE_TEST_DISPLAY:-:99}" -export XDG_RUNTIME_DIR="${FEATURE_TEST_XDG_RUNTIME_DIR:-/tmp/devcontainers-x11-runtime}" -export XAUTHORITY="${FEATURE_TEST_XAUTHORITY:-/tmp/devcontainers-xauthority.test}" -export PULSE_SERVER="${FEATURE_TEST_PULSE_SERVER:-unix:${XDG_RUNTIME_DIR}/pulse-native}" -xwayland_xauthority="${FEATURE_TEST_XWAYLAND_XAUTHORITY:-${XDG_RUNTIME_DIR}/.mutter-Xwaylandauth.test}" - -cleanup_paths+=("${XAUTHORITY}" "${XDG_RUNTIME_DIR}") - -mkdir -p "$(dirname "${XAUTHORITY}")" -mkdir -p "${XDG_RUNTIME_DIR}" -chmod 700 "${XDG_RUNTIME_DIR}" -touch "${XAUTHORITY}" -touch "${xwayland_xauthority}" -touch "${XDG_RUNTIME_DIR}/pulse-native" - -for feature in linux-x11-forwarding ros2; do - devcontainer features test \ - --base-image "${base_image}" \ - --features "${feature}" \ - --project-folder "${features_root}" -done From 358ba7fd18048a34e9bc2102b6ca57dd11f52fcf Mon Sep 17 00:00:00 2001 From: Allison Thackston <73732028+althack@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:26:08 -0700 Subject: [PATCH 3/4] Consolidate feature test workflows --- .github/workflows/test-main.yaml | 35 -------------------- .github/workflows/test-pr.yaml | 56 ++++++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 42 deletions(-) delete mode 100644 .github/workflows/test-main.yaml diff --git a/.github/workflows/test-main.yaml b/.github/workflows/test-main.yaml deleted file mode 100644 index 2ccb739..0000000 --- a/.github/workflows/test-main.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: "CI - Test ROS 2 Distribution Matrix" - -on: - push: - # branches: - # - main - workflow_dispatch: - -jobs: - ros2-matrix: - name: "ROS 2 ${{ matrix.scenario }}" - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - scenario: - - humble - - kilted - - lyrical - steps: - - uses: actions/checkout@v6 - - - name: Install devcontainer CLI - run: npm install --global @devcontainers/cli@0.88.0 - - - name: Validate ROS 2 configuration - run: python3 features/test/ros2/validate_config.py - - - name: Test ROS 2 ${{ matrix.scenario }} - run: | - devcontainer features test \ - --features ros2 \ - --project-folder features \ - --skip-autogenerated \ - --filter "${{ matrix.scenario }}" diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index dfd00a0..f210ba0 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -34,9 +34,6 @@ jobs: - name: Prepare X11 test environment run: bash features/test/linux-x11-forwarding/prepare-host.sh - - name: Validate ROS 2 configuration - run: python3 features/test/ros2/validate_config.py - - name: Test every Feature with default options run: | devcontainer features test \ @@ -44,6 +41,28 @@ jobs: --project-folder features \ --skip-scenarios + - name: Package Features + run: | + devcontainer features package features/src \ + --output-folder "${RUNNER_TEMP}/features" \ + --force-clean-output-folder + + x11-feature-tests: + runs-on: ubuntu-latest + env: + DISPLAY: ":99" + XDG_RUNTIME_DIR: /tmp/devcontainers-x11-runtime + XAUTHORITY: /tmp/devcontainers-xauthority.test + PULSE_SERVER: unix:/tmp/devcontainers-x11-runtime/pulse-native + steps: + - uses: actions/checkout@v6 + + - name: Install devcontainer CLI + run: npm install --global @devcontainers/cli@0.88.0 + + - name: Prepare X11 test environment + run: bash features/test/linux-x11-forwarding/prepare-host.sh + - name: Test X11 option scenarios run: | devcontainer features test \ @@ -51,11 +70,32 @@ jobs: --project-folder features \ --skip-autogenerated - - name: Package Features + ros2-feature-tests: + name: "ROS 2 ${{ matrix.scenario }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + scenario: + - humble + - kilted + - lyrical + steps: + - uses: actions/checkout@v6 + + - name: Install devcontainer CLI + run: npm install --global @devcontainers/cli@0.88.0 + + - name: Validate ROS 2 configuration + run: python3 features/test/ros2/validate_config.py + + - name: Test ROS 2 ${{ matrix.scenario }} run: | - devcontainer features package features/src \ - --output-folder "${RUNNER_TEMP}/features" \ - --force-clean-output-folder + devcontainer features test \ + --features ros2 \ + --project-folder features \ + --skip-autogenerated \ + --filter "${{ matrix.scenario }}" docs-check: runs-on: ubuntu-latest @@ -87,6 +127,8 @@ jobs: needs: - docs-check - feature-tests + - x11-feature-tests + - ros2-feature-tests - smoke-test if: always() runs-on: ubuntu-latest From b6b693999eaeb608a3218b8cdf40a58da1bc4745 Mon Sep 17 00:00:00 2001 From: Allison Thackston <73732028+althack@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:38:33 -0700 Subject: [PATCH 4/4] Refine feature test workflow and tasks --- .github/workflows/test-pr.yaml | 96 +++++++++++++++++----------------- .vscode/tasks.json | 32 ++++++------ 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index f210ba0..7c92e6c 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -18,7 +18,7 @@ jobs: with: template: "gz" - feature-tests: + x11-feature-tests: runs-on: ubuntu-latest env: DISPLAY: ":99" @@ -34,43 +34,32 @@ jobs: - name: Prepare X11 test environment run: bash features/test/linux-x11-forwarding/prepare-host.sh - - name: Test every Feature with default options + - name: Test X11 Feature run: | devcontainer features test \ - --base-image mcr.microsoft.com/devcontainers/base:ubuntu-24.04 \ - --project-folder features \ - --skip-scenarios - - - name: Package Features - run: | - devcontainer features package features/src \ - --output-folder "${RUNNER_TEMP}/features" \ - --force-clean-output-folder + --features linux-x11-forwarding \ + --project-folder features - x11-feature-tests: + ros2-default-test: runs-on: ubuntu-latest - env: - DISPLAY: ":99" - XDG_RUNTIME_DIR: /tmp/devcontainers-x11-runtime - XAUTHORITY: /tmp/devcontainers-xauthority.test - PULSE_SERVER: unix:/tmp/devcontainers-x11-runtime/pulse-native steps: - uses: actions/checkout@v6 - name: Install devcontainer CLI run: npm install --global @devcontainers/cli@0.88.0 - - name: Prepare X11 test environment - run: bash features/test/linux-x11-forwarding/prepare-host.sh + - name: Validate ROS 2 configuration + run: python3 features/test/ros2/validate_config.py - - name: Test X11 option scenarios + - name: Test ROS 2 default run: | devcontainer features test \ - --features linux-x11-forwarding \ + --base-image mcr.microsoft.com/devcontainers/base:ubuntu-24.04 \ + --features ros2 \ --project-folder features \ - --skip-autogenerated + --skip-scenarios - ros2-feature-tests: + ros2-scenario-tests: name: "ROS 2 ${{ matrix.scenario }}" runs-on: ubuntu-latest strategy: @@ -86,9 +75,6 @@ jobs: - name: Install devcontainer CLI run: npm install --global @devcontainers/cli@0.88.0 - - name: Validate ROS 2 configuration - run: python3 features/test/ros2/validate_config.py - - name: Test ROS 2 ${{ matrix.scenario }} run: | devcontainer features test \ @@ -97,6 +83,20 @@ jobs: --skip-autogenerated \ --filter "${{ matrix.scenario }}" + feature-package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install devcontainer CLI + run: npm install --global @devcontainers/cli@0.88.0 + + - name: Package Features + run: | + devcontainer features package features/src \ + --output-folder "${RUNNER_TEMP}/features" \ + --force-clean-output-folder + docs-check: runs-on: ubuntu-latest steps: @@ -126,9 +126,10 @@ jobs: complete: needs: - docs-check - - feature-tests + - feature-package - x11-feature-tests - - ros2-feature-tests + - ros2-default-test + - ros2-scenario-tests - smoke-test if: always() runs-on: ubuntu-latest @@ -136,28 +137,29 @@ jobs: - name: Verify required jobs succeeded env: SMOKE_TEST_RESULT: ${{ needs.smoke-test.result }} - FEATURE_TESTS_RESULT: ${{ needs.feature-tests.result }} + X11_FEATURE_TESTS_RESULT: ${{ needs.x11-feature-tests.result }} + ROS2_DEFAULT_TEST_RESULT: ${{ needs.ros2-default-test.result }} + ROS2_SCENARIO_TESTS_RESULT: ${{ needs.ros2-scenario-tests.result }} + FEATURE_PACKAGE_RESULT: ${{ needs.feature-package.result }} DOCS_CHECK_RESULT: ${{ needs.docs-check.result }} run: | set -euo pipefail - echo "smoke_test: ${SMOKE_TEST_RESULT}" - echo "feature_tests: ${FEATURE_TESTS_RESULT}" - echo "docs_check: ${DOCS_CHECK_RESULT}" - - if [[ "${SMOKE_TEST_RESULT}" != "success" ]]; then - echo "Required job 'smoke_test' did not succeed." - exit 1 - fi - - if [[ "${FEATURE_TESTS_RESULT}" != "success" ]]; then - echo "Required job 'feature_tests' did not succeed." - exit 1 - fi - - if [[ "${DOCS_CHECK_RESULT}" != "success" ]]; then - echo "Required job 'docs-check' did not succeed." - exit 1 - fi + check_result() { + local job_name="$1" + local result="$2" + echo "${job_name}: ${result}" + if [[ "${result}" != "success" ]]; then + echo "Required job '${job_name}' did not succeed." + exit 1 + fi + } + + check_result "smoke-test" "${SMOKE_TEST_RESULT}" + check_result "x11-feature-tests" "${X11_FEATURE_TESTS_RESULT}" + check_result "ros2-default-test" "${ROS2_DEFAULT_TEST_RESULT}" + check_result "ros2-scenario-tests" "${ROS2_SCENARIO_TESTS_RESULT}" + check_result "feature-package" "${FEATURE_PACKAGE_RESULT}" + check_result "docs-check" "${DOCS_CHECK_RESULT}" echo "All required jobs completed successfully." diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0272182..ff009dd 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -104,10 +104,10 @@ "problemMatcher": [] }, { - "label": "Test Feature Defaults", + "label": "Test ROS 2 Default", "dependsOrder": "sequence", "dependsOn": [ - "Prepare X11 Test Environment" + "Validate ROS 2 Configuration" ], "type": "process", "command": "devcontainer", @@ -116,23 +116,19 @@ "test", "--base-image", "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "--features", + "ros2", "--project-folder", "./features", "--skip-scenarios" ], "options": { - "cwd": "${workspaceFolder}", - "env": { - "DISPLAY": ":99", - "XDG_RUNTIME_DIR": "/tmp/devcontainers-x11-runtime", - "XAUTHORITY": "/tmp/devcontainers-xauthority.test", - "PULSE_SERVER": "unix:/tmp/devcontainers-x11-runtime/pulse-native" - } + "cwd": "${workspaceFolder}" }, "problemMatcher": [] }, { - "label": "Test X11 Scenarios", + "label": "Test X11 Feature", "dependsOrder": "sequence", "dependsOn": [ "Prepare X11 Test Environment" @@ -145,8 +141,7 @@ "--features", "linux-x11-forwarding", "--project-folder", - "./features", - "--skip-autogenerated" + "./features" ], "options": { "cwd": "${workspaceFolder}", @@ -161,6 +156,10 @@ }, { "label": "Test ROS 2 Compatibility Matrix", + "dependsOrder": "sequence", + "dependsOn": [ + "Validate ROS 2 Configuration" + ], "type": "process", "command": "devcontainer", "args": [ @@ -178,12 +177,11 @@ "problemMatcher": [] }, { - "label": "Test PR Feature Suite", - "dependsOrder": "sequence", + "label": "Test Feature Suite", "dependsOn": [ - "Validate ROS 2 Configuration", - "Test Feature Defaults", - "Test X11 Scenarios" + "Test X11 Feature", + "Test ROS 2 Default", + "Test ROS 2 Compatibility Matrix" ], "group": { "kind": "test",