From 2372dd97329c0c4bc6579bfbf125f90b52e462c4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 5 Apr 2026 10:54:17 +0000 Subject: [PATCH 01/19] feat(gofumpt): init Generated using boilerplate templates. --- src/gofumpt/devcontainer-feature.json | 18 +++ src/gofumpt/install.sh | 21 ++++ src/gofumpt/library_scripts.sh | 173 ++++++++++++++++++++++++++ test/gofumpt/scenarios.json | 16 +++ test/gofumpt/test.sh | 9 ++ test/gofumpt/test_debian.sh | 9 ++ test/gofumpt/test_specific_version.sh | 9 ++ 7 files changed, 255 insertions(+) create mode 100644 src/gofumpt/devcontainer-feature.json create mode 100755 src/gofumpt/install.sh create mode 100644 src/gofumpt/library_scripts.sh create mode 100644 test/gofumpt/scenarios.json create mode 100755 test/gofumpt/test.sh create mode 100755 test/gofumpt/test_debian.sh create mode 100755 test/gofumpt/test_specific_version.sh diff --git a/src/gofumpt/devcontainer-feature.json b/src/gofumpt/devcontainer-feature.json new file mode 100644 index 000000000..b7ab1991a --- /dev/null +++ b/src/gofumpt/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "gofumpt", + "version": "1.0.0", + "name": "gofumpt (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/gofumpt", + "description": "A stricter gofmt", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/gofumpt/install.sh b/src/gofumpt/install.sh new file mode 100755 index 000000000..757067be9 --- /dev/null +++ b/src/gofumpt/install.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='mvdan/gofumpt' --option binaryNames='gofumpt' --option version="$VERSION" + +echo 'Done!' diff --git a/src/gofumpt/library_scripts.sh b/src/gofumpt/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/gofumpt/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/test/gofumpt/scenarios.json b/test/gofumpt/scenarios.json new file mode 100644 index 000000000..ddb717af8 --- /dev/null +++ b/test/gofumpt/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "gofumpt": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "gofumpt": { + "version": "0.9.1" + } + } + } +} diff --git a/test/gofumpt/test.sh b/test/gofumpt/test.sh new file mode 100755 index 000000000..5b32aea25 --- /dev/null +++ b/test/gofumpt/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "gofumpt is installed" gofumpt --version + +reportResults diff --git a/test/gofumpt/test_debian.sh b/test/gofumpt/test_debian.sh new file mode 100755 index 000000000..5b32aea25 --- /dev/null +++ b/test/gofumpt/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "gofumpt is installed" gofumpt --version + +reportResults diff --git a/test/gofumpt/test_specific_version.sh b/test/gofumpt/test_specific_version.sh new file mode 100755 index 000000000..cc1d6d3bb --- /dev/null +++ b/test/gofumpt/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "gofumpt version is equal to 0.9.1" sh -c "gofumpt --version | grep '0.9.1'" + +reportResults From 434b9b09c2949d8f905a805b6b412aa333e6eac9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 5 Apr 2026 10:58:06 +0000 Subject: [PATCH 02/19] feat(golangci-lint): init Generated using boilerplate templates. --- src/golangci-lint/devcontainer-feature.json | 18 ++ src/golangci-lint/install.sh | 21 +++ src/golangci-lint/library_scripts.sh | 173 ++++++++++++++++++++ test/golangci-lint/scenarios.json | 16 ++ test/golangci-lint/test.sh | 9 + test/golangci-lint/test_debian.sh | 9 + test/golangci-lint/test_specific_version.sh | 9 + 7 files changed, 255 insertions(+) create mode 100644 src/golangci-lint/devcontainer-feature.json create mode 100755 src/golangci-lint/install.sh create mode 100644 src/golangci-lint/library_scripts.sh create mode 100644 test/golangci-lint/scenarios.json create mode 100755 test/golangci-lint/test.sh create mode 100755 test/golangci-lint/test_debian.sh create mode 100755 test/golangci-lint/test_specific_version.sh diff --git a/src/golangci-lint/devcontainer-feature.json b/src/golangci-lint/devcontainer-feature.json new file mode 100644 index 000000000..da686f2d9 --- /dev/null +++ b/src/golangci-lint/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "golangci-lint", + "version": "1.0.0", + "name": "golangci-lint (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/golangci-lint", + "description": "Fast linters runner for Go", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/golangci-lint/install.sh b/src/golangci-lint/install.sh new file mode 100755 index 000000000..e88801f01 --- /dev/null +++ b/src/golangci-lint/install.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='golangci/golangci-lint' --option binaryNames='golangci-lint' --option version="$VERSION" + +echo 'Done!' diff --git a/src/golangci-lint/library_scripts.sh b/src/golangci-lint/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/golangci-lint/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/test/golangci-lint/scenarios.json b/test/golangci-lint/scenarios.json new file mode 100644 index 000000000..e02c2fb21 --- /dev/null +++ b/test/golangci-lint/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "golangci-lint": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "golangci-lint": { + "version": "2.11.3" + } + } + } +} diff --git a/test/golangci-lint/test.sh b/test/golangci-lint/test.sh new file mode 100755 index 000000000..0308f30a2 --- /dev/null +++ b/test/golangci-lint/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "golangci-lint is installed" golangci-lint --version + +reportResults diff --git a/test/golangci-lint/test_debian.sh b/test/golangci-lint/test_debian.sh new file mode 100755 index 000000000..0308f30a2 --- /dev/null +++ b/test/golangci-lint/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "golangci-lint is installed" golangci-lint --version + +reportResults diff --git a/test/golangci-lint/test_specific_version.sh b/test/golangci-lint/test_specific_version.sh new file mode 100755 index 000000000..e673274ff --- /dev/null +++ b/test/golangci-lint/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "golangci-lint version is equal to 2.11.3" sh -c "golangci-lint --version | grep '2.11.3'" + +reportResults From 5363113cb487fc7a4f0c3c3f31fcfa853d9abd31 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:29:47 +0200 Subject: [PATCH 03/19] feat(claude-code): install from script as npm is deprecated by Anthropic --- src/claude-code/bootstrap.sh | 158 ++++++++++++++++++++ src/claude-code/devcontainer-feature.json | 4 +- src/claude-code/install.sh | 23 +-- src/claude-code/library_scripts.sh | 173 ---------------------- 4 files changed, 168 insertions(+), 190 deletions(-) create mode 100644 src/claude-code/bootstrap.sh delete mode 100644 src/claude-code/library_scripts.sh diff --git a/src/claude-code/bootstrap.sh b/src/claude-code/bootstrap.sh new file mode 100644 index 000000000..5cc7babab --- /dev/null +++ b/src/claude-code/bootstrap.sh @@ -0,0 +1,158 @@ +#!/bin/bash + +set -e + +# Parse command line arguments +TARGET="$1" # Optional target parameter + +# Validate target if provided +if [[ -n "$TARGET" ]] && [[ ! "$TARGET" =~ ^(stable|latest|[0-9]+\.[0-9]+\.[0-9]+(-[^[:space:]]+)?)$ ]]; then + echo "Usage: $0 [stable|latest|VERSION]" >&2 + exit 1 +fi + +GCS_BUCKET="https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases" +DOWNLOAD_DIR="$HOME/.claude/downloads" + +# Check for required dependencies +DOWNLOADER="" +if command -v curl >/dev/null 2>&1; then + DOWNLOADER="curl" +elif command -v wget >/dev/null 2>&1; then + DOWNLOADER="wget" +else + echo "Either curl or wget is required but neither is installed" >&2 + exit 1 +fi + +# Check if jq is available (optional) +HAS_JQ=false +if command -v jq >/dev/null 2>&1; then + HAS_JQ=true +fi + +# Download function that works with both curl and wget +download_file() { + local url="$1" + local output="$2" + + if [ "$DOWNLOADER" = "curl" ]; then + if [ -n "$output" ]; then + curl -fsSL -o "$output" "$url" + else + curl -fsSL "$url" + fi + elif [ "$DOWNLOADER" = "wget" ]; then + if [ -n "$output" ]; then + wget -q -O "$output" "$url" + else + wget -q -O - "$url" + fi + else + return 1 + fi +} + +# Simple JSON parser for extracting checksum when jq is not available +get_checksum_from_manifest() { + local json="$1" + local platform="$2" + + # Normalize JSON to single line and extract checksum + json=$(echo "$json" | tr -d '\n\r\t' | sed 's/ \+/ /g') + + # Extract checksum for platform using bash regex + if [[ $json =~ \"$platform\"[^}]*\"checksum\"[[:space:]]*:[[:space:]]*\"([a-f0-9]{64})\" ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + + return 1 +} + +# Detect platform +case "$(uname -s)" in + Darwin) os="darwin" ;; + Linux) os="linux" ;; + MINGW*|MSYS*|CYGWIN*) echo "Windows is not supported by this script. See https://code.claude.com/docs for installation options." >&2; exit 1 ;; + *) echo "Unsupported operating system: $(uname -s). See https://code.claude.com/docs for supported platforms." >&2; exit 1 ;; +esac + +case "$(uname -m)" in + x86_64|amd64) arch="x64" ;; + arm64|aarch64) arch="arm64" ;; + *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; +esac + +# Detect Rosetta 2 on macOS: if the shell is running as x64 under Rosetta on an ARM Mac, +# download the native arm64 binary instead of the x64 one +if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then + if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then + arch="arm64" + fi +fi + +# Check for musl on Linux and adjust platform accordingly +if [ "$os" = "linux" ]; then + if [ -f /lib/libc.musl-x86_64.so.1 ] || [ -f /lib/libc.musl-aarch64.so.1 ] || ldd /bin/ls 2>&1 | grep -q musl; then + platform="linux-${arch}-musl" + else + platform="linux-${arch}" + fi +else + platform="${os}-${arch}" +fi +mkdir -p "$DOWNLOAD_DIR" + +# Always download latest version (which has the most up-to-date installer) +version=$(download_file "$GCS_BUCKET/latest") + +# Download manifest and extract checksum +manifest_json=$(download_file "$GCS_BUCKET/$version/manifest.json") + +# Use jq if available, otherwise fall back to pure bash parsing +if [ "$HAS_JQ" = true ]; then + checksum=$(echo "$manifest_json" | jq -r ".platforms[\"$platform\"].checksum // empty") +else + checksum=$(get_checksum_from_manifest "$manifest_json" "$platform") +fi + +# Validate checksum format (SHA256 = 64 hex characters) +if [ -z "$checksum" ] || [[ ! "$checksum" =~ ^[a-f0-9]{64}$ ]]; then + echo "Platform $platform not found in manifest" >&2 + exit 1 +fi + +# Download and verify +binary_path="$DOWNLOAD_DIR/claude-$version-$platform" +if ! download_file "$GCS_BUCKET/$version/$platform/claude" "$binary_path"; then + echo "Download failed" >&2 + rm -f "$binary_path" + exit 1 +fi + +# Pick the right checksum tool +if [ "$os" = "darwin" ]; then + actual=$(shasum -a 256 "$binary_path" | cut -d' ' -f1) +else + actual=$(sha256sum "$binary_path" | cut -d' ' -f1) +fi + +if [ "$actual" != "$checksum" ]; then + echo "Checksum verification failed" >&2 + rm -f "$binary_path" + exit 1 +fi + +chmod +x "$binary_path" + +# Run claude install to set up launcher and shell integration +echo "Setting up Claude Code..." +"$binary_path" install ${TARGET:+"$TARGET"} + +# Clean up downloaded file +rm -f "$binary_path" + +echo "" +echo "✅ Installation complete!" +echo "" diff --git a/src/claude-code/devcontainer-feature.json b/src/claude-code/devcontainer-feature.json index 281bec53c..1e970d379 100644 --- a/src/claude-code/devcontainer-feature.json +++ b/src/claude-code/devcontainer-feature.json @@ -1,7 +1,7 @@ { "id": "claude-code", - "version": "1.0.0", - "name": "claude-code (via npm)", + "version": "2.0.0", + "name": "claude-code", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/claude-code", "description": "Claude Code is an agentic coding tool that lives in your terminal", "options": { diff --git a/src/claude-code/install.sh b/src/claude-code/install.sh index adc2d3820..ecc6b50df 100644 --- a/src/claude-code/install.sh +++ b/src/claude-code/install.sh @@ -2,20 +2,13 @@ set -e -source ./library_scripts.sh - -# nanolayer is a cli utility which keeps container layers as small as possible -# source code: https://github.com/devcontainers-extra/nanolayer -# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, -# and if missing - will download a temporary copy that automatically get deleted at the end -# of the script -ensure_nanolayer nanolayer_location "v0.5.6" - -# Example nanolayer installation via devcontainer-feature -$nanolayer_location \ - install \ - devcontainer-feature \ - "ghcr.io/devcontainers-extra/features/npm-package:1.0.4" \ - --option package='@anthropic-ai/claude-code' --option version="$VERSION" +# Install via Anthropic's recommended method +# Script downloaded from them: +# https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/bootstrap.sh +bash "$(dirname "$0")/bootstrap.sh" "$VERSION" + +# Copy the binary to /usr/local/bin for global access +# It's installed in /root/.local/bin/claude and there's no option to override the install location +cp "$HOME/.local/bin/claude" /usr/local/bin/claude echo 'Done!' diff --git a/src/claude-code/library_scripts.sh b/src/claude-code/library_scripts.sh deleted file mode 100644 index f6d0760d7..000000000 --- a/src/claude-code/library_scripts.sh +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env bash - -clean_download() { - # The purpose of this function is to download a file with minimal impact on container layer size - # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a - # temporary manner, and making sure to - # 1. uninstall the downloader at the return of the function - # 2. revert back any changes to the package installer database/cache (for example apt-get lists) - # The above steps will minimize the leftovers being created while installing the downloader - # Supported distros: - # debian/ubuntu/alpine - - url=$1 - output_location=$2 - tempdir=$(mktemp -d) - downloader_installed="" - - function _apt_get_install() { - tempdir=$1 - - # copy current state of apt list - in order to revert back later (minimize contianer layer size) - cp -p -R /var/lib/apt/lists $tempdir - apt-get update -y - apt-get -y install --no-install-recommends wget ca-certificates - } - - function _apt_get_cleanup() { - tempdir=$1 - - echo "removing wget" - apt-get -y purge wget --auto-remove - - echo "revert back apt lists" - rm -rf /var/lib/apt/lists/* - rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists - } - - function _apk_install() { - tempdir=$1 - # copy current state of apk cache - in order to revert back later (minimize contianer layer size) - cp -p -R /var/cache/apk $tempdir - - apk add --no-cache wget - } - - function _apk_cleanup() { - tempdir=$1 - - echo "removing wget" - apk del wget - } - # try to use either wget or curl if one of them already installer - if type curl >/dev/null 2>&1; then - downloader=curl - elif type wget >/dev/null 2>&1; then - downloader=wget - else - downloader="" - fi - - # in case none of them is installed, install wget temporarly - if [ -z $downloader ]; then - if [ -x "/usr/bin/apt-get" ]; then - _apt_get_install $tempdir - elif [ -x "/sbin/apk" ]; then - _apk_install $tempdir - else - echo "distro not supported" - exit 1 - fi - downloader="wget" - downloader_installed="true" - fi - - if [ $downloader = "wget" ]; then - wget -q $url -O $output_location - else - curl -sfL $url -o $output_location - fi - - # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because - # alpine lack bash, and RETURN is not a valid signal under sh shell - if ! [ -z $downloader_installed ]; then - if [ -x "/usr/bin/apt-get" ]; then - _apt_get_cleanup $tempdir - elif [ -x "/sbin/apk" ]; then - _apk_cleanup $tempdir - else - echo "distro not supported" - exit 1 - fi - fi - -} - -ensure_nanolayer() { - # Ensure existance of the nanolayer cli program - local variable_name=$1 - - local required_version=$2 - # normalize version - if ! [[ $required_version == v* ]]; then - required_version=v$required_version - fi - - local nanolayer_location="" - - # If possible - try to use an already installed nanolayer - if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then - if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then - if type nanolayer >/dev/null 2>&1; then - echo "Found a pre-existing nanolayer in PATH" - nanolayer_location=nanolayer - fi - elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then - nanolayer_location=${NANOLAYER_CLI_LOCATION} - echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" - fi - - # make sure its of the required version - if ! [[ -z "${nanolayer_location}" ]]; then - local current_version - current_version=$($nanolayer_location --version) - if ! [[ $current_version == v* ]]; then - current_version=v$current_version - fi - - if ! [ $current_version == $required_version ]; then - echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" - nanolayer_location="" - fi - fi - - fi - - # If not previuse installation found, download it temporarly and delete at the end of the script - if [[ -z "${nanolayer_location}" ]]; then - - if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then - tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) - - clean_up() { - ARG=$? - rm -rf $tmp_dir - exit $ARG - } - trap clean_up EXIT - - if [ -x "/sbin/apk" ]; then - clib_type=musl - else - clib_type=gnu - fi - - tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz - - # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed - clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename - - tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" - chmod a+x $tmp_dir/nanolayer - nanolayer_location=$tmp_dir/nanolayer - - else - echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" - exit 1 - fi - fi - - # Expose outside the resolved location - declare -g ${variable_name}=$nanolayer_location - -} From 36b88e42802ab6f1310dfaf693fd928fb5908da1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 11 Apr 2026 07:16:21 +0000 Subject: [PATCH 04/19] feat(archon): init Generated using boilerplate templates. --- src/archon/devcontainer-feature.json | 18 +++ src/archon/install.sh | 21 ++++ src/archon/library_scripts.sh | 173 +++++++++++++++++++++++++++ test/archon/scenarios.json | 16 +++ test/archon/test.sh | 9 ++ test/archon/test_debian.sh | 9 ++ test/archon/test_specific_version.sh | 9 ++ 7 files changed, 255 insertions(+) create mode 100644 src/archon/devcontainer-feature.json create mode 100755 src/archon/install.sh create mode 100644 src/archon/library_scripts.sh create mode 100644 test/archon/scenarios.json create mode 100755 test/archon/test.sh create mode 100755 test/archon/test_debian.sh create mode 100755 test/archon/test_specific_version.sh diff --git a/src/archon/devcontainer-feature.json b/src/archon/devcontainer-feature.json new file mode 100644 index 000000000..7fdedbb3f --- /dev/null +++ b/src/archon/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "archon", + "version": "1.0.0", + "name": "archon (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/archon", + "description": "The first open-source harness builder for AI coding. Make AI coding deterministic and repeatable.", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/archon/install.sh b/src/archon/install.sh new file mode 100755 index 000000000..94ce02311 --- /dev/null +++ b/src/archon/install.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='coleam00/Archon' --option binaryNames='archon' --option version="$VERSION" + +echo 'Done!' diff --git a/src/archon/library_scripts.sh b/src/archon/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/archon/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/test/archon/scenarios.json b/test/archon/scenarios.json new file mode 100644 index 000000000..ab5f5292f --- /dev/null +++ b/test/archon/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "archon": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "archon": { + "version": "0.3.4" + } + } + } +} diff --git a/test/archon/test.sh b/test/archon/test.sh new file mode 100755 index 000000000..2739ca851 --- /dev/null +++ b/test/archon/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "archon is installed" archon version + +reportResults diff --git a/test/archon/test_debian.sh b/test/archon/test_debian.sh new file mode 100755 index 000000000..2739ca851 --- /dev/null +++ b/test/archon/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "archon is installed" archon version + +reportResults diff --git a/test/archon/test_specific_version.sh b/test/archon/test_specific_version.sh new file mode 100755 index 000000000..68dd898f3 --- /dev/null +++ b/test/archon/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "archon version is equal to 0.3.4" sh -c "archon version | grep '0.3.4'" + +reportResults From 576fcf3dc9dfe9eec84e38695fb65f27bac79bdd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 10 Apr 2026 08:49:15 +0000 Subject: [PATCH 05/19] feat(worktrunk): init Generated using boilerplate templates. --- src/worktrunk/devcontainer-feature.json | 18 +++ src/worktrunk/install.sh | 22 +++ src/worktrunk/library_scripts.sh | 173 ++++++++++++++++++++++++ test/worktrunk/scenarios.json | 16 +++ test/worktrunk/test.sh | 9 ++ test/worktrunk/test_debian.sh | 9 ++ test/worktrunk/test_specific_version.sh | 9 ++ 7 files changed, 256 insertions(+) create mode 100644 src/worktrunk/devcontainer-feature.json create mode 100755 src/worktrunk/install.sh create mode 100644 src/worktrunk/library_scripts.sh create mode 100644 test/worktrunk/scenarios.json create mode 100755 test/worktrunk/test.sh create mode 100755 test/worktrunk/test_debian.sh create mode 100755 test/worktrunk/test_specific_version.sh diff --git a/src/worktrunk/devcontainer-feature.json b/src/worktrunk/devcontainer-feature.json new file mode 100644 index 000000000..47811b0f1 --- /dev/null +++ b/src/worktrunk/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "worktrunk", + "version": "1.0.0", + "name": "worktrunk (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/worktrunk", + "description": "Worktrunk is a CLI for Git worktree management, designed for parallel AI agent workflows", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/worktrunk/install.sh b/src/worktrunk/install.sh new file mode 100755 index 000000000..efdd12f8b --- /dev/null +++ b/src/worktrunk/install.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='max-sixty/worktrunk' --option binaryNames='wt' --option version="$VERSION" \ + --option assetRegex='worktrunk.*(tar).*' + +echo 'Done!' diff --git a/src/worktrunk/library_scripts.sh b/src/worktrunk/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/worktrunk/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/test/worktrunk/scenarios.json b/test/worktrunk/scenarios.json new file mode 100644 index 000000000..a40ab0053 --- /dev/null +++ b/test/worktrunk/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "worktrunk": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "worktrunk": { + "version": "0.35.2" + } + } + } +} diff --git a/test/worktrunk/test.sh b/test/worktrunk/test.sh new file mode 100755 index 000000000..b04829f67 --- /dev/null +++ b/test/worktrunk/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "wt is installed" wt --version + +reportResults diff --git a/test/worktrunk/test_debian.sh b/test/worktrunk/test_debian.sh new file mode 100755 index 000000000..b04829f67 --- /dev/null +++ b/test/worktrunk/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "wt is installed" wt --version + +reportResults diff --git a/test/worktrunk/test_specific_version.sh b/test/worktrunk/test_specific_version.sh new file mode 100755 index 000000000..8a47888e5 --- /dev/null +++ b/test/worktrunk/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "wt version is equal to 0.35.2" sh -c "wt --version 2>&1 | grep '0.35.2'" + +reportResults From e19435b45a8f026beb476bee073f73ae391d3b52 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 10 May 2026 10:25:02 +0000 Subject: [PATCH 06/19] feat(opengrep): init Generated using boilerplate templates. --- src/opengrep/devcontainer-feature.json | 18 +++ src/opengrep/install.sh | 21 +++ src/opengrep/library_scripts.sh | 173 +++++++++++++++++++++++++ test/opengrep/scenarios.json | 16 +++ test/opengrep/test.sh | 9 ++ test/opengrep/test_debian.sh | 9 ++ test/opengrep/test_specific_version.sh | 9 ++ 7 files changed, 255 insertions(+) create mode 100644 src/opengrep/devcontainer-feature.json create mode 100755 src/opengrep/install.sh create mode 100644 src/opengrep/library_scripts.sh create mode 100644 test/opengrep/scenarios.json create mode 100755 test/opengrep/test.sh create mode 100755 test/opengrep/test_debian.sh create mode 100755 test/opengrep/test_specific_version.sh diff --git a/src/opengrep/devcontainer-feature.json b/src/opengrep/devcontainer-feature.json new file mode 100644 index 000000000..594ddc49c --- /dev/null +++ b/src/opengrep/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "opengrep", + "version": "1.0.0", + "name": "opengrep (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/opengrep", + "description": "Static code analysis engine to find security issues in code.", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/src/opengrep/install.sh b/src/opengrep/install.sh new file mode 100755 index 000000000..3a6013285 --- /dev/null +++ b/src/opengrep/install.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='opengrep/opengrep' --option binaryNames='opengrep' --option version="$VERSION" + +echo 'Done!' diff --git a/src/opengrep/library_scripts.sh b/src/opengrep/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/opengrep/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/test/opengrep/scenarios.json b/test/opengrep/scenarios.json new file mode 100644 index 000000000..666eabe99 --- /dev/null +++ b/test/opengrep/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "opengrep": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "opengrep": { + "version": "1.18.0" + } + } + } +} diff --git a/test/opengrep/test.sh b/test/opengrep/test.sh new file mode 100755 index 000000000..54223790c --- /dev/null +++ b/test/opengrep/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "opengrep is installed" opengrep --version + +reportResults diff --git a/test/opengrep/test_debian.sh b/test/opengrep/test_debian.sh new file mode 100755 index 000000000..54223790c --- /dev/null +++ b/test/opengrep/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "opengrep is installed" opengrep --version + +reportResults diff --git a/test/opengrep/test_specific_version.sh b/test/opengrep/test_specific_version.sh new file mode 100755 index 000000000..d7b816f74 --- /dev/null +++ b/test/opengrep/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "opengrep version is equal to 1.18.0" sh -c "opengrep --version | grep '1.18.0'" + +reportResults From 2de1553da54e1e15fe826e024e31e9d835c498f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 10:38:59 +0000 Subject: [PATCH 07/19] fix(opengrep): use assetRegex to avoid ambiguous asset matches Excludes .tar.gz and .cert assets, keeping only the plain manylinux binary. https://claude.ai/code/session_01S5yubvvERsngN7BHF2Am7b --- src/opengrep/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/opengrep/install.sh b/src/opengrep/install.sh index 3a6013285..9b7efc1e1 100755 --- a/src/opengrep/install.sh +++ b/src/opengrep/install.sh @@ -16,6 +16,6 @@ $nanolayer_location \ install \ devcontainer-feature \ "ghcr.io/devcontainers-extra/features/gh-release:1" \ - --option repo='opengrep/opengrep' --option binaryNames='opengrep' --option version="$VERSION" + --option repo='opengrep/opengrep' --option binaryNames='opengrep' --option version="$VERSION" --option assetRegex='opengrep_manylinux_[^.]*$' echo 'Done!' From f95a96742ea4c975070a8fa0af1d6ff922a0dbd4 Mon Sep 17 00:00:00 2001 From: Stephen Rudolph Date: Tue, 12 May 2026 09:33:06 -0500 Subject: [PATCH 08/19] chore: Caddy install.sh dependency updates Only the Go version was strictly necessary due to https://github.com/golangci/golangci-lint/issues/6555, but bumping the others while here. Signed-off-by: Stephen Rudolph --- src/caddy/install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/caddy/install.sh b/src/caddy/install.sh index d5d7f9357..fd47c6175 100755 --- a/src/caddy/install.sh +++ b/src/caddy/install.sh @@ -8,13 +8,13 @@ set -e # `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, # and if missing - will download a temporary copy that automatically get deleted at the end # of the script -ensure_nanolayer nanolayer_location "v0.5.4" +ensure_nanolayer nanolayer_location "v0.5.6" $nanolayer_location \ install \ devcontainer-feature \ - "ghcr.io/devcontainers/features/go:1.1.3" \ + "ghcr.io/devcontainers/features/go:1.3.4" \ --option version="$GOLANGVERSION" @@ -22,7 +22,7 @@ $nanolayer_location \ $nanolayer_location \ install \ devcontainer-feature \ - "ghcr.io/devcontainers-extra/features/gh-release:1.0.25" \ + "ghcr.io/devcontainers-extra/features/gh-release:1.0.26" \ --option repo='caddyserver/caddy' --option binaryNames='caddy' --option version="$VERSION" From a075d8fc7dad82e20d6fddb1e79d14a99949e417 Mon Sep 17 00:00:00 2001 From: Stephen Rudolph Date: Tue, 12 May 2026 14:02:52 -0500 Subject: [PATCH 09/19] fix: updating the Caddy feature version because the dependencies were updated Signed-off-by: Stephen Rudolph --- src/caddy/devcontainer-feature.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/caddy/devcontainer-feature.json b/src/caddy/devcontainer-feature.json index e61553eb1..be9dd4712 100644 --- a/src/caddy/devcontainer-feature.json +++ b/src/caddy/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "caddy", - "version": "1.0.10", + "version": "1.0.11", "name": "Caddy (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/caddy", "description": "Caddy is a powerful, enterprise-ready, open source web server with automatic HTTPS.", From 3acf26818f3dfab084d69e3dad045949568e9b1f Mon Sep 17 00:00:00 2001 From: Sudhanva Sreesha Date: Sat, 16 May 2026 19:14:09 -0400 Subject: [PATCH 10/19] Add `bazel-compile-commands` --- src/bazel-compile-commands/README.md | 42 ++++++++ .../devcontainer-feature.json | 21 ++++ src/bazel-compile-commands/install.sh | 52 +++++++++ src/bazel-compile-commands/library_scripts.sh | 100 ++++++++++++++++++ test/bazel-compile-commands/scenarios.json | 25 +++++ test/bazel-compile-commands/test.sh | 10 ++ test/bazel-compile-commands/test_debian.sh | 10 ++ .../test_specific_version.sh | 11 ++ 8 files changed, 271 insertions(+) create mode 100644 src/bazel-compile-commands/README.md create mode 100644 src/bazel-compile-commands/devcontainer-feature.json create mode 100755 src/bazel-compile-commands/install.sh create mode 100644 src/bazel-compile-commands/library_scripts.sh create mode 100644 test/bazel-compile-commands/scenarios.json create mode 100755 test/bazel-compile-commands/test.sh create mode 100755 test/bazel-compile-commands/test_debian.sh create mode 100644 test/bazel-compile-commands/test_specific_version.sh diff --git a/src/bazel-compile-commands/README.md b/src/bazel-compile-commands/README.md new file mode 100644 index 000000000..dcb9da00c --- /dev/null +++ b/src/bazel-compile-commands/README.md @@ -0,0 +1,42 @@ +# bazel-compile-commands (via Github Releases) (bazel-compile-commands) + +Generates `compile_commands.json` from Bazel builds, enabling accurate code intelligence (go-to-definition, cross-references, diagnostics) in editors that support the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/). + +## Example Usage + +```json +"features": { + "ghcr.io/devcontainers-extra/features/bazel-compile-commands:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +| ---------- | -------------------------------------------------------------------------------------------------------------------------------- | ------ | ------------- | +| version | Version to install. Accepts `latest`, a numeric version like `0.22.4`, or the full release tag `bazel-compile-commands-v0.22.4`. | string | latest | + +## Install behaviour + +The feature selects the best available package for the current environment: + +| Environment | Package used | +| -------------------- | ------------------------------- | +| Ubuntu Noble (24.04) | Native `.deb` via `apt-get` | +| Other Linux | Generic `.zip` (amd64 or arm64) | +| macOS | Universal `.zip` | + +## Pairing with Bazel + +This feature pairs naturally with [`ghcr.io/devcontainers-community/features/bazel:1`](https://github.com/devcontainers-community/features-bazel), which installs Bazelisk and Buildifier. When both are present in `devcontainer.json`, the Bazel feature is installed first. + +```json +"features": { + "ghcr.io/devcontainers-community/features/bazel:1": {}, + "ghcr.io/devcontainers-extra/features/bazel-compile-commands:1": {} +} +``` + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/src/bazel-compile-commands/devcontainer-feature.json b/src/bazel-compile-commands/devcontainer-feature.json new file mode 100644 index 000000000..d8149697b --- /dev/null +++ b/src/bazel-compile-commands/devcontainer-feature.json @@ -0,0 +1,21 @@ +{ + "id": "bazel-compile-commands", + "version": "1.0.0", + "name": "bazel-compile-commands (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/bazel-compile-commands", + "description": "Generates compile_commands.json from Bazel builds. Installs the appropriate binary for the current platform and CPU architecture: a native .deb on Ubuntu Noble (24.04), a generic zip on other Linux distros, and the macOS universal binary on Darwin.", + "options": { + "version": { + "default": "latest", + "description": "Version to install. Accepts 'latest', a numeric version like '0.22.4', or the full release tag 'bazel-compile-commands-v0.22.4'.", + "proposals": [ + "latest", + "0.22.4" + ], + "type": "string" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers-community/features/bazel" + ] +} diff --git a/src/bazel-compile-commands/install.sh b/src/bazel-compile-commands/install.sh new file mode 100755 index 000000000..73525fe22 --- /dev/null +++ b/src/bazel-compile-commands/install.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +BINARY_NAME="bazel-compile-commands" +GITHUB_REPO="kiron1/bazel-compile-commands" +VERSION="${VERSION:-"latest"}" + +PLATFORM="$(detect_platform)" +ARCH="$(detect_arch)" +ensure_packages curl ca-certificates +TAG="$(resolve_tag "${VERSION}" "${GITHUB_REPO}" "${BINARY_NAME}")" + +# Extract the numeric version from the tag (e.g. "bazel-compile-commands-v0.22.4" → "0.22.4") +FILE_VERSION="${TAG#${BINARY_NAME}-v}" + +BASE_URL="https://github.com/${GITHUB_REPO}/releases/download/${TAG}" + +echo "Installing ${BINARY_NAME} ${TAG} for ${PLATFORM}/${ARCH}..." + +USE_DEB=false +ZIP_ASSET="" + +if [ "${PLATFORM}" = "linux" ]; then + # On Ubuntu Noble (24.04), prefer the native .deb package; fall back to the generic .zip. + if command -v apt-get >/dev/null 2>&1 && \ + [ -f /etc/os-release ] && \ + grep -qi "noble" /etc/os-release; then + USE_DEB=true + else + ZIP_ASSET="linux_${ARCH}" + fi +elif [ "${PLATFORM}" = "macos" ]; then + ZIP_ASSET="macos_universal" +fi + +if [ "${USE_DEB}" = true ]; then + echo "Ubuntu Noble detected – installing via .deb package..." + TMP_DEB=$(mktemp --suffix=.deb) + curl -fsSL "${BASE_URL}/${BINARY_NAME}_${FILE_VERSION}-noble_${ARCH}.deb" -o "${TMP_DEB}" + apt_get_update + apt-get install -y "${TMP_DEB}" + rm -f "${TMP_DEB}" + apt_cleanup +else + echo "Installing via .zip (${ZIP_ASSET})..." + install_via_zip "${BASE_URL}/${BINARY_NAME}_${FILE_VERSION}-${ZIP_ASSET}.zip" "${BINARY_NAME}" +fi + +echo "Done! ${BINARY_NAME} installed to /usr/local/bin/${BINARY_NAME}" diff --git a/src/bazel-compile-commands/library_scripts.sh b/src/bazel-compile-commands/library_scripts.sh new file mode 100644 index 000000000..9a09c2232 --- /dev/null +++ b/src/bazel-compile-commands/library_scripts.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +# Adapted from https://github.com/devcontainers-community/features-bazel/blob/main/lib.sh +# which itself traces back to the official devcontainers/features node install script. + +# Runs apt-get update only when the apt lists cache is empty, avoiding redundant network hits. +apt_get_update() { + if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update -y + fi +} + +# Installs the given packages only if they are not already installed (checked via dpkg). +# Debian/Ubuntu only; calls apt_get_update lazily rather than unconditionally. +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update + apt-get -y install --no-install-recommends "$@" + fi +} + +# Removes apt list files to keep the container layer small. +apt_cleanup() { + rm -rf /var/lib/apt/lists/* +} + +# Ensures the given packages are installed, using apt-get on Debian/Ubuntu or apk on Alpine. +ensure_packages() { + if command -v apt-get >/dev/null 2>&1; then + check_packages "$@" + elif command -v apk >/dev/null 2>&1; then + apk add --no-cache "$@" + else + echo "No supported package manager found to install: $*" >&2 + exit 1 + fi +} + +detect_platform() { + local os + os="$(uname -s)" + case "${os}" in + Linux) echo "linux" ;; + Darwin) echo "macos" ;; + *) echo "Unsupported platform: ${os}" >&2; exit 1 ;; + esac +} + +detect_arch() { + local arch + arch="$(uname -m)" + case "${arch}" in + x86_64) echo "amd64" ;; + aarch64 | arm64) echo "arm64" ;; + *) echo "Unsupported architecture: ${arch}" >&2; exit 1 ;; + esac +} + +# Resolves a user-provided version string to the exact GitHub release tag. +# Accepted inputs: "latest", "0.22.4", "v0.22.4", or "bazel-compile-commands-v0.22.4" +resolve_tag() { + local version="$1" + local repo="$2" + local tag_prefix="$3" # e.g. "bazel-compile-commands" + + if [ "${version}" = "latest" ]; then + curl -fsSL "https://api.github.com/repos/${repo}/releases/latest" \ + | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' + elif [[ "${version}" == ${tag_prefix}-* ]]; then + echo "${version}" + elif [[ "${version}" == v* ]]; then + echo "${tag_prefix}-${version}" + else + echo "${tag_prefix}-v${version}" + fi +} + +# Downloads a GitHub release zip asset, extracts a named binary, and places it in /usr/local/bin. +# +# Usage: install_via_zip +# +# zip_url - Direct URL to the .zip asset on GitHub Releases. +# binary_name - Name of the binary inside the archive (e.g. "bazel-compile-commands"). +# +# unzip is auto-installed if not already present, and the temporary download +# directory is cleaned up on exit. +install_via_zip() { + local zip_url="$1" + local binary_name="$2" + + ensure_packages unzip + + local tmp_dir + tmp_dir=$(mktemp -d) + curl -fsSL "${zip_url}" -o "${tmp_dir}/${binary_name}.zip" + unzip -j "${tmp_dir}/${binary_name}.zip" -d "${tmp_dir}" + install -m 0755 "${tmp_dir}/${binary_name}" /usr/local/bin/ + rm -rf "${tmp_dir}" +} diff --git a/test/bazel-compile-commands/scenarios.json b/test/bazel-compile-commands/scenarios.json new file mode 100644 index 000000000..a893dbd73 --- /dev/null +++ b/test/bazel-compile-commands/scenarios.json @@ -0,0 +1,25 @@ +{ + "test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "ghcr.io/devcontainers-community/features/bazel:1": {}, + "bazel-compile-commands": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "ghcr.io/devcontainers-community/features/bazel:1": {}, + "bazel-compile-commands": { + "version": "0.22.4" + } + } + }, + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "ghcr.io/devcontainers-community/features/bazel:1": {}, + "bazel-compile-commands": {} + } + } +} diff --git a/test/bazel-compile-commands/test.sh b/test/bazel-compile-commands/test.sh new file mode 100755 index 000000000..2b2cfd07a --- /dev/null +++ b/test/bazel-compile-commands/test.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "bazel-compile-commands is installed" bazel-compile-commands --version +check "bazelisk is installed" bazelisk --version + +reportResults diff --git a/test/bazel-compile-commands/test_debian.sh b/test/bazel-compile-commands/test_debian.sh new file mode 100755 index 000000000..2b2cfd07a --- /dev/null +++ b/test/bazel-compile-commands/test_debian.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "bazel-compile-commands is installed" bazel-compile-commands --version +check "bazelisk is installed" bazelisk --version + +reportResults diff --git a/test/bazel-compile-commands/test_specific_version.sh b/test/bazel-compile-commands/test_specific_version.sh new file mode 100644 index 000000000..a42376b83 --- /dev/null +++ b/test/bazel-compile-commands/test_specific_version.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +which bazel-compile-commands +check "bazel-compile-commands version is 0.22.4" sh -c "bazel-compile-commands --version 2>&1 | grep '0.22.4'" +check "bazelisk is installed" bazelisk --version + +reportResults From d0b4881e87f98c0c84856d9f18115fcc20547d07 Mon Sep 17 00:00:00 2001 From: Sudhanva Sreesha Date: Sun, 17 May 2026 15:10:24 +0000 Subject: [PATCH 11/19] Refactor: Smplify `install.sh` to use `gh-release` pattern and standarize `library_scripts.sh`. --- .../devcontainer-feature.json | 4 +- src/bazel-compile-commands/install.sh | 70 ++--- src/bazel-compile-commands/library_scripts.sh | 239 ++++++++++++------ 3 files changed, 182 insertions(+), 131 deletions(-) diff --git a/src/bazel-compile-commands/devcontainer-feature.json b/src/bazel-compile-commands/devcontainer-feature.json index d8149697b..7d1963345 100644 --- a/src/bazel-compile-commands/devcontainer-feature.json +++ b/src/bazel-compile-commands/devcontainer-feature.json @@ -3,11 +3,11 @@ "version": "1.0.0", "name": "bazel-compile-commands (via Github Releases)", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/bazel-compile-commands", - "description": "Generates compile_commands.json from Bazel builds. Installs the appropriate binary for the current platform and CPU architecture: a native .deb on Ubuntu Noble (24.04), a generic zip on other Linux distros, and the macOS universal binary on Darwin.", + "description": "Generates compile_commands.json from Bazel builds.", "options": { "version": { "default": "latest", - "description": "Version to install. Accepts 'latest', a numeric version like '0.22.4', or the full release tag 'bazel-compile-commands-v0.22.4'.", + "description": "Version to install. Accepts 'latest' or a numeric version like '0.22.4'.", "proposals": [ "latest", "0.22.4" diff --git a/src/bazel-compile-commands/install.sh b/src/bazel-compile-commands/install.sh index 73525fe22..de53af507 100755 --- a/src/bazel-compile-commands/install.sh +++ b/src/bazel-compile-commands/install.sh @@ -2,51 +2,29 @@ set -e -source ./library_scripts.sh - -BINARY_NAME="bazel-compile-commands" -GITHUB_REPO="kiron1/bazel-compile-commands" -VERSION="${VERSION:-"latest"}" - -PLATFORM="$(detect_platform)" -ARCH="$(detect_arch)" -ensure_packages curl ca-certificates -TAG="$(resolve_tag "${VERSION}" "${GITHUB_REPO}" "${BINARY_NAME}")" - -# Extract the numeric version from the tag (e.g. "bazel-compile-commands-v0.22.4" → "0.22.4") -FILE_VERSION="${TAG#${BINARY_NAME}-v}" - -BASE_URL="https://github.com/${GITHUB_REPO}/releases/download/${TAG}" - -echo "Installing ${BINARY_NAME} ${TAG} for ${PLATFORM}/${ARCH}..." - -USE_DEB=false -ZIP_ASSET="" - -if [ "${PLATFORM}" = "linux" ]; then - # On Ubuntu Noble (24.04), prefer the native .deb package; fall back to the generic .zip. - if command -v apt-get >/dev/null 2>&1 && \ - [ -f /etc/os-release ] && \ - grep -qi "noble" /etc/os-release; then - USE_DEB=true - else - ZIP_ASSET="linux_${ARCH}" - fi -elif [ "${PLATFORM}" = "macos" ]; then - ZIP_ASSET="macos_universal" -fi - -if [ "${USE_DEB}" = true ]; then - echo "Ubuntu Noble detected – installing via .deb package..." - TMP_DEB=$(mktemp --suffix=.deb) - curl -fsSL "${BASE_URL}/${BINARY_NAME}_${FILE_VERSION}-noble_${ARCH}.deb" -o "${TMP_DEB}" - apt_get_update - apt-get install -y "${TMP_DEB}" - rm -f "${TMP_DEB}" - apt_cleanup -else - echo "Installing via .zip (${ZIP_ASSET})..." - install_via_zip "${BASE_URL}/${BINARY_NAME}_${FILE_VERSION}-${ZIP_ASSET}.zip" "${BINARY_NAME}" +. ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +# Upstream tags use a non-standard "bazel-compile-commands-v" format. Normalise +# any plain version (e.g. "0.22.4" or "v0.22.4") so nanolayer can match it exactly. +if [[ "${VERSION}" != "latest" && "${VERSION}" != bazel-compile-commands-* ]]; then + VERSION="bazel-compile-commands-v${VERSION#v}" fi -echo "Done! ${BINARY_NAME} installed to /usr/local/bin/${BINARY_NAME}" +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='kiron1/bazel-compile-commands' \ + --option binaryNames='bazel-compile-commands' \ + --option version="$VERSION" \ + --option assetRegex='linux_' \ + --option releaseTagRegex='bazel-compile-commands-v' + +echo 'Done!' diff --git a/src/bazel-compile-commands/library_scripts.sh b/src/bazel-compile-commands/library_scripts.sh index 9a09c2232..f6d0760d7 100644 --- a/src/bazel-compile-commands/library_scripts.sh +++ b/src/bazel-compile-commands/library_scripts.sh @@ -1,100 +1,173 @@ #!/usr/bin/env bash -# Adapted from https://github.com/devcontainers-community/features-bazel/blob/main/lib.sh -# which itself traces back to the official devcontainers/features node install script. +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine -# Runs apt-get update only when the apt lists cache is empty, avoiding redundant network hits. -apt_get_update() { - if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then - echo "Running apt-get update..." + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir apt-get update -y - fi -} + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } -# Installs the given packages only if they are not already installed (checked via dpkg). -# Debian/Ubuntu only; calls apt_get_update lazily rather than unconditionally. -check_packages() { - if ! dpkg -s "$@" > /dev/null 2>&1; then - apt_get_update - apt-get -y install --no-install-recommends "$@" + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" fi -} -# Removes apt list files to keep the container layer small. -apt_cleanup() { - rm -rf /var/lib/apt/lists/* -} + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi -# Ensures the given packages are installed, using apt-get on Debian/Ubuntu or apk on Alpine. -ensure_packages() { - if command -v apt-get >/dev/null 2>&1; then - check_packages "$@" - elif command -v apk >/dev/null 2>&1; then - apk add --no-cache "$@" + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location else - echo "No supported package manager found to install: $*" >&2 - exit 1 + curl -sfL $url -o $output_location fi -} -detect_platform() { - local os - os="$(uname -s)" - case "${os}" in - Linux) echo "linux" ;; - Darwin) echo "macos" ;; - *) echo "Unsupported platform: ${os}" >&2; exit 1 ;; - esac -} + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi -detect_arch() { - local arch - arch="$(uname -m)" - case "${arch}" in - x86_64) echo "amd64" ;; - aarch64 | arm64) echo "arm64" ;; - *) echo "Unsupported architecture: ${arch}" >&2; exit 1 ;; - esac } -# Resolves a user-provided version string to the exact GitHub release tag. -# Accepted inputs: "latest", "0.22.4", "v0.22.4", or "bazel-compile-commands-v0.22.4" -resolve_tag() { - local version="$1" - local repo="$2" - local tag_prefix="$3" # e.g. "bazel-compile-commands" - - if [ "${version}" = "latest" ]; then - curl -fsSL "https://api.github.com/repos/${repo}/releases/latest" \ - | grep '"tag_name"' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/' - elif [[ "${version}" == ${tag_prefix}-* ]]; then - echo "${version}" - elif [[ "${version}" == v* ]]; then - echo "${tag_prefix}-${version}" - else - echo "${tag_prefix}-v${version}" +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version fi -} -# Downloads a GitHub release zip asset, extracts a named binary, and places it in /usr/local/bin. -# -# Usage: install_via_zip -# -# zip_url - Direct URL to the .zip asset on GitHub Releases. -# binary_name - Name of the binary inside the archive (e.g. "bazel-compile-commands"). -# -# unzip is auto-installed if not already present, and the temporary download -# directory is cleaned up on exit. -install_via_zip() { - local zip_url="$1" - local binary_name="$2" - - ensure_packages unzip - - local tmp_dir - tmp_dir=$(mktemp -d) - curl -fsSL "${zip_url}" -o "${tmp_dir}/${binary_name}.zip" - unzip -j "${tmp_dir}/${binary_name}.zip" -d "${tmp_dir}" - install -m 0755 "${tmp_dir}/${binary_name}" /usr/local/bin/ - rm -rf "${tmp_dir}" + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + } From c00fcc4b480d80ae09f9bf5ae99b19cef57f2a52 Mon Sep 17 00:00:00 2001 From: Oscar Brouwer Date: Wed, 20 May 2026 14:40:18 +0000 Subject: [PATCH 12/19] Create new feature for TFLint --- src/tflint/README.md | 24 ++++ src/tflint/devcontainer-feature.json | 20 ++++ src/tflint/install.sh | 21 ++++ src/tflint/library_scripts.sh | 173 +++++++++++++++++++++++++++ test/tflint/scenarios.json | 16 +++ test/tflint/test.sh | 9 ++ test/tflint/test_default.sh | 9 ++ test/tflint/test_specific_version.sh | 9 ++ 8 files changed, 281 insertions(+) create mode 100644 src/tflint/README.md create mode 100644 src/tflint/devcontainer-feature.json create mode 100755 src/tflint/install.sh create mode 100644 src/tflint/library_scripts.sh create mode 100644 test/tflint/scenarios.json create mode 100755 test/tflint/test.sh create mode 100755 test/tflint/test_default.sh create mode 100755 test/tflint/test_specific_version.sh diff --git a/src/tflint/README.md b/src/tflint/README.md new file mode 100644 index 000000000..507b023bc --- /dev/null +++ b/src/tflint/README.md @@ -0,0 +1,24 @@ + +# TFLint (via Github Releases) (tflint) + +TFLint is a framework and each feature is provided by plugins, the key features are as follows: + +- Find possible errors (like invalid instance types) for Major Cloud providers (AWS/Azure/GCP). +- Warn about deprecated syntax, unused declarations. +- Enforce best practices, naming conventions. + +## Example Usage + +```json +"features": { + "ghcr.io/devcontainers-extra/features/tflint:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|------------|--------------------------------|--------|---------------| +| version | Select the version to install. | string | latest | + +--- diff --git a/src/tflint/devcontainer-feature.json b/src/tflint/devcontainer-feature.json new file mode 100644 index 000000000..88865b937 --- /dev/null +++ b/src/tflint/devcontainer-feature.json @@ -0,0 +1,20 @@ +{ + "id": "tflint", + "version": "1.0.0", + "name": "tflint (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/tflint", + "description": "A Pluggable Terraform Linter", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers-extra/features/gh-release" + ] +} \ No newline at end of file diff --git a/src/tflint/install.sh b/src/tflint/install.sh new file mode 100755 index 000000000..bae05e0ed --- /dev/null +++ b/src/tflint/install.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='terraform-linters/tflint' --option binaryNames='tflint' --option version="$VERSION" + +echo 'Done!' diff --git a/src/tflint/library_scripts.sh b/src/tflint/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/src/tflint/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/test/tflint/scenarios.json b/test/tflint/scenarios.json new file mode 100644 index 000000000..75c49af43 --- /dev/null +++ b/test/tflint/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_default": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "tflint": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "tflint": { + "version": "0.62.1" + } + } + } +} \ No newline at end of file diff --git a/test/tflint/test.sh b/test/tflint/test.sh new file mode 100755 index 000000000..70c491bac --- /dev/null +++ b/test/tflint/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "something is installed" tflint --version + +reportResults diff --git a/test/tflint/test_default.sh b/test/tflint/test_default.sh new file mode 100755 index 000000000..96f1aaddd --- /dev/null +++ b/test/tflint/test_default.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "tflint is installed" tflint --version + +reportResults diff --git a/test/tflint/test_specific_version.sh b/test/tflint/test_specific_version.sh new file mode 100755 index 000000000..dd7e01ce9 --- /dev/null +++ b/test/tflint/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "tflint version is equal to 0.62.1" sh -c "tflint --version | grep '0.62.1'" + +reportResults From 44042adaf239dc74edf9c446c08e2cd91b08e9f6 Mon Sep 17 00:00:00 2001 From: Oscar Brouwer Date: Thu, 21 May 2026 07:49:50 +0200 Subject: [PATCH 13/19] Use another version than the latest Co-authored-by: Arek Kalandyk <36413794+koralowiec@users.noreply.github.com> Signed-off-by: Oscar Brouwer --- test/tflint/scenarios.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tflint/scenarios.json b/test/tflint/scenarios.json index 75c49af43..7cb7266ef 100644 --- a/test/tflint/scenarios.json +++ b/test/tflint/scenarios.json @@ -9,7 +9,7 @@ "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { "tflint": { - "version": "0.62.1" + "version": "0.62.0" } } } From 1f4fe5162f245cc23124810c94fb982b16438694 Mon Sep 17 00:00:00 2001 From: Oscar Brouwer Date: Thu, 21 May 2026 07:50:00 +0200 Subject: [PATCH 14/19] Use another version than the latest Co-authored-by: Arek Kalandyk <36413794+koralowiec@users.noreply.github.com> Signed-off-by: Oscar Brouwer --- test/tflint/test_specific_version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tflint/test_specific_version.sh b/test/tflint/test_specific_version.sh index dd7e01ce9..742971673 100755 --- a/test/tflint/test_specific_version.sh +++ b/test/tflint/test_specific_version.sh @@ -4,6 +4,6 @@ set -e source dev-container-features-test-lib -check "tflint version is equal to 0.62.1" sh -c "tflint --version | grep '0.62.1'" +check "tflint version is equal to 0.62.0" sh -c "tflint --version | grep '0.62.0'" reportResults From 58b09f119c00287c854e4afc315d7d6324e7563e Mon Sep 17 00:00:00 2001 From: twsl <45483159+twsl@users.noreply.github.com> Date: Tue, 26 May 2026 22:45:14 +0200 Subject: [PATCH 15/19] Update download URL for claude-code releases Signed-off-by: twsl <45483159+twsl@users.noreply.github.com> --- src/claude-code/bootstrap.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/claude-code/bootstrap.sh b/src/claude-code/bootstrap.sh index 5cc7babab..375daea51 100644 --- a/src/claude-code/bootstrap.sh +++ b/src/claude-code/bootstrap.sh @@ -11,7 +11,7 @@ if [[ -n "$TARGET" ]] && [[ ! "$TARGET" =~ ^(stable|latest|[0-9]+\.[0-9]+\.[0-9] exit 1 fi -GCS_BUCKET="https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases" +DOWNLOAD_BASE_URL="https://downloads.claude.ai/claude-code-releases" DOWNLOAD_DIR="$HOME/.claude/downloads" # Check for required dependencies @@ -105,10 +105,10 @@ fi mkdir -p "$DOWNLOAD_DIR" # Always download latest version (which has the most up-to-date installer) -version=$(download_file "$GCS_BUCKET/latest") +version=$(download_file "$DOWNLOAD_BASE_URL/latest") # Download manifest and extract checksum -manifest_json=$(download_file "$GCS_BUCKET/$version/manifest.json") +manifest_json=$(download_file "$DOWNLOAD_BASE_URL/$version/manifest.json") # Use jq if available, otherwise fall back to pure bash parsing if [ "$HAS_JQ" = true ]; then @@ -125,7 +125,7 @@ fi # Download and verify binary_path="$DOWNLOAD_DIR/claude-$version-$platform" -if ! download_file "$GCS_BUCKET/$version/$platform/claude" "$binary_path"; then +if ! download_file "$DOWNLOAD_BASE_URL/$version/$platform/claude" "$binary_path"; then echo "Download failed" >&2 rm -f "$binary_path" exit 1 From a87a829d008318159efad1c43a4c9469ecb03307 Mon Sep 17 00:00:00 2001 From: twsl <45483159+twsl@users.noreply.github.com> Date: Wed, 27 May 2026 10:01:22 +0200 Subject: [PATCH 16/19] Bump version from 2.0.0 to 2.0.1 Signed-off-by: twsl <45483159+twsl@users.noreply.github.com> --- src/claude-code/devcontainer-feature.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/claude-code/devcontainer-feature.json b/src/claude-code/devcontainer-feature.json index 1e970d379..be0f0ab5d 100644 --- a/src/claude-code/devcontainer-feature.json +++ b/src/claude-code/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "claude-code", - "version": "2.0.0", + "version": "2.0.1", "name": "claude-code", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/claude-code", "description": "Claude Code is an agentic coding tool that lives in your terminal", @@ -22,4 +22,4 @@ } }, "installsAfter": [] -} \ No newline at end of file +} From 5962aef5dd561bc0d5fe8d634b43aab48d51ba53 Mon Sep 17 00:00:00 2001 From: Brian Helba Date: Wed, 17 Jun 2026 11:10:53 -0400 Subject: [PATCH 17/19] Fix claude-code installing files owned by root `bootstrap.sh` was running as root, so `~/.claude/`, `~/.local/bin/claude`, and `~/.local/share/claude/` all ended up under `/root/` owned by root. The devcontainer user had no access to config or shell integration. Use the devcontainer spec variables `_REMOTE_USER` and `_REMOTE_USER_HOME` to run the install as the devcontainer user, following the pattern used by `asdf-package`, `homebrew-package`, and other features in this repo. --- src/claude-code/devcontainer-feature.json | 2 +- src/claude-code/install.sh | 14 +++++++++++--- test/claude-code/test_debian.sh | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/claude-code/devcontainer-feature.json b/src/claude-code/devcontainer-feature.json index be0f0ab5d..ffc36ad8d 100644 --- a/src/claude-code/devcontainer-feature.json +++ b/src/claude-code/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "claude-code", - "version": "2.0.1", + "version": "2.0.2", "name": "claude-code", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/claude-code", "description": "Claude Code is an agentic coding tool that lives in your terminal", diff --git a/src/claude-code/install.sh b/src/claude-code/install.sh index ecc6b50df..1ab410ca0 100644 --- a/src/claude-code/install.sh +++ b/src/claude-code/install.sh @@ -5,10 +5,18 @@ set -e # Install via Anthropic's recommended method # Script downloaded from them: # https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/bootstrap.sh -bash "$(dirname "$0")/bootstrap.sh" "$VERSION" + +# Resolve the absolute path before su changes the working directory +BOOTSTRAP_SCRIPT="$(pwd)/bootstrap.sh" + +# Run as the devcontainer user so that ~/.claude/, ~/.local/bin/claude, and +# ~/.local/share/claude/ are owned by the right user, not root. +su - "$_REMOTE_USER" < Date: Mon, 29 Jun 2026 11:10:50 +0200 Subject: [PATCH 18/19] fix: install fails with unset _REMOTE_USER_HOME --- src/claude-code/devcontainer-feature.json | 2 +- src/claude-code/install.sh | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/claude-code/devcontainer-feature.json b/src/claude-code/devcontainer-feature.json index ffc36ad8d..d0288726e 100644 --- a/src/claude-code/devcontainer-feature.json +++ b/src/claude-code/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "claude-code", - "version": "2.0.2", + "version": "2.0.3", "name": "claude-code", "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/claude-code", "description": "Claude Code is an agentic coding tool that lives in your terminal", diff --git a/src/claude-code/install.sh b/src/claude-code/install.sh index 1ab410ca0..e31ac039d 100644 --- a/src/claude-code/install.sh +++ b/src/claude-code/install.sh @@ -2,6 +2,13 @@ set -e +# Make sure that variable _REMOTE_USER_HOME is properly set +if [ -z "$_REMOTE_USER_HOME" ]; then + # shellcheck disable=2016 # this is intentional as the expansion should happen in the context of the new session + _REMOTE_USER_HOME="$(su - "$_REMOTE_USER" -c 'echo "$HOME"')" + export _REMOTE_USER_HOME +fi + # Install via Anthropic's recommended method # Script downloaded from them: # https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/bootstrap.sh From 7a2bdb8531643d2e356a214ad012b1ff9ea580e1 Mon Sep 17 00:00:00 2001 From: Kerolos Atallah Date: Wed, 5 Aug 2026 22:49:16 +0300 Subject: [PATCH 19/19] feat(outagedeck): add CLI feature --- src/outagedeck/README.md | 34 ++++++ src/outagedeck/devcontainer-feature.json | 21 ++++ src/outagedeck/install.sh | 19 ++++ src/outagedeck/library_scripts.sh | 131 +++++++++++++++++++++++ test/outagedeck/scenarios.json | 16 +++ test/outagedeck/test.sh | 9 ++ test/outagedeck/test_default.sh | 10 ++ test/outagedeck/test_specific_version.sh | 9 ++ 8 files changed, 249 insertions(+) create mode 100644 src/outagedeck/README.md create mode 100644 src/outagedeck/devcontainer-feature.json create mode 100755 src/outagedeck/install.sh create mode 100755 src/outagedeck/library_scripts.sh create mode 100644 test/outagedeck/scenarios.json create mode 100755 test/outagedeck/test.sh create mode 100755 test/outagedeck/test_default.sh create mode 100755 test/outagedeck/test_specific_version.sh diff --git a/src/outagedeck/README.md b/src/outagedeck/README.md new file mode 100644 index 000000000..5c0bbadc3 --- /dev/null +++ b/src/outagedeck/README.md @@ -0,0 +1,34 @@ +# OutageDeck CLI (via GitHub Releases) (outagedeck) + +Check the live status of 170+ cloud and SaaS providers from a terminal or CI +script. OutageDeck normalizes each vendor's official status feed; it does not +replace synthetic monitoring. + +## Example usage + +```json +"features": { + "ghcr.io/devcontainers-extra/features/outagedeck:1": {} +} +``` + +Then query one or more provider slugs: + +```bash +outagedeck status aws cloudflare github openai +``` + +For structured CI output and a nonzero exit code when a provider is down: + +```bash +outagedeck status --json --fail-on=outage aws github openai +``` + +Find more examples in the [OutageDeck CLI repository](https://github.com/outagedeck/cli) +or review the [API documentation](https://outagedeck.com/developers/api?utm_source=devcontainers-extra&utm_medium=feature&utm_campaign=devcontainer_feature). + +## Options + +| Options Id | Description | Type | Default Value | +|------------|----------------------------------------------|--------|---------------| +| version | Select the OutageDeck CLI version to install. | string | latest | diff --git a/src/outagedeck/devcontainer-feature.json b/src/outagedeck/devcontainer-feature.json new file mode 100644 index 000000000..c8d1c231a --- /dev/null +++ b/src/outagedeck/devcontainer-feature.json @@ -0,0 +1,21 @@ +{ + "id": "outagedeck", + "version": "1.0.0", + "name": "OutageDeck CLI (via GitHub Releases)", + "documentationURL": "https://github.com/devcontainers-extra/features/tree/main/src/outagedeck", + "description": "Check the live status of cloud and SaaS providers from a terminal or CI script.", + "options": { + "version": { + "default": "latest", + "description": "Select the OutageDeck CLI version to install.", + "proposals": [ + "latest", + "0.1.0" + ], + "type": "string" + } + }, + "installsAfter": [ + "ghcr.io/devcontainers-extra/features/gh-release" + ] +} diff --git a/src/outagedeck/install.sh b/src/outagedeck/install.sh new file mode 100755 index 000000000..fd472f850 --- /dev/null +++ b/src/outagedeck/install.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a CLI utility which keeps container layers as small as possible. +# Source: https://github.com/devcontainers-extra/nanolayer +ensure_nanolayer nanolayer_location "v0.5.6" + +"$nanolayer_location" \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='outagedeck/cli' \ + --option binaryNames='outagedeck' \ + --option version="$VERSION" + +echo 'Done!' diff --git a/src/outagedeck/library_scripts.sh b/src/outagedeck/library_scripts.sh new file mode 100755 index 000000000..98e0da825 --- /dev/null +++ b/src/outagedeck/library_scripts.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash + +clean_download() { + local url=$1 + local output_location=$2 + local tempdir + tempdir=$(mktemp -d) + local downloader_installed="" + + _apt_get_install() { + local apt_tempdir=$1 + cp -p -R /var/lib/apt/lists "$apt_tempdir" + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + _apt_get_cleanup() { + local apt_tempdir=$1 + apt-get -y purge wget --auto-remove + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists + mv "$apt_tempdir/lists" /var/lib/apt/lists + } + + _apk_install() { + local apk_tempdir=$1 + cp -p -R /var/cache/apk "$apk_tempdir" + apk add --no-cache wget + } + + _apk_cleanup() { + apk del wget + } + + local downloader + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + if [ -z "$downloader" ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install "$tempdir" + elif [ -x "/sbin/apk" ]; then + _apk_install "$tempdir" + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ "$downloader" = "wget" ]; then + wget -q "$url" -O "$output_location" + else + curl -sfL "$url" -o "$output_location" + fi + + if [ -n "$downloader_installed" ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup "$tempdir" + elif [ -x "/sbin/apk" ]; then + _apk_cleanup + fi + fi +} + +ensure_nanolayer() { + local variable_name=$1 + local required_version=$2 + + if [[ $required_version != v* ]]; then + required_version="v$required_version" + fi + + local resolved_nanolayer="" + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION:-}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION:-}" ]]; then + if type nanolayer >/dev/null 2>&1; then + resolved_nanolayer=nanolayer + fi + elif [[ -f "$NANOLAYER_CLI_LOCATION" && -x "$NANOLAYER_CLI_LOCATION" ]]; then + resolved_nanolayer=$NANOLAYER_CLI_LOCATION + fi + + if [[ -n "$resolved_nanolayer" ]]; then + local current_version + current_version=$($resolved_nanolayer --version) + if [[ $current_version != v* ]]; then + current_version="v$current_version" + fi + if [[ $current_version != "$required_version" ]]; then + resolved_nanolayer="" + fi + fi + fi + + if [[ -z "$resolved_nanolayer" ]]; then + if [[ "$(uname -sm)" != "Linux x86_64" && "$(uname -sm)" != "Linux aarch64" ]]; then + echo "No nanolayer binary for $(uname -sm)" + exit 1 + fi + + local nanolayer_tempdir + nanolayer_tempdir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + clean_up() { + local exit_code=$? + rm -rf "$nanolayer_tempdir" + exit "$exit_code" + } + trap clean_up EXIT + + local clib_type=gnu + if [ -x "/sbin/apk" ]; then + clib_type=musl + fi + local tar_filename="nanolayer-$(uname -m)-unknown-linux-$clib_type.tgz" + clean_download \ + "https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename" \ + "$nanolayer_tempdir/$tar_filename" + tar xfz "$nanolayer_tempdir/$tar_filename" -C "$nanolayer_tempdir" + chmod a+x "$nanolayer_tempdir/nanolayer" + resolved_nanolayer="$nanolayer_tempdir/nanolayer" + fi + + declare -g "$variable_name=$resolved_nanolayer" +} diff --git a/test/outagedeck/scenarios.json b/test/outagedeck/scenarios.json new file mode 100644 index 000000000..bb01553fa --- /dev/null +++ b/test/outagedeck/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_default": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "outagedeck": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "outagedeck": { + "version": "0.1.0" + } + } + } +} diff --git a/test/outagedeck/test.sh b/test/outagedeck/test.sh new file mode 100755 index 000000000..b0120b3d2 --- /dev/null +++ b/test/outagedeck/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "outagedeck is installed" outagedeck --version + +reportResults diff --git a/test/outagedeck/test_default.sh b/test/outagedeck/test_default.sh new file mode 100755 index 000000000..2602aad9b --- /dev/null +++ b/test/outagedeck/test_default.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "outagedeck is installed" outagedeck --version +check "outagedeck status command is available" sh -c "outagedeck help | grep 'outagedeck status'" + +reportResults diff --git a/test/outagedeck/test_specific_version.sh b/test/outagedeck/test_specific_version.sh new file mode 100755 index 000000000..d8f1a0491 --- /dev/null +++ b/test/outagedeck/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "outagedeck version is 0.1.0" sh -c "outagedeck --version | grep '0.1.0'" + +reportResults