From a8c770c61159d07542e3d0b1ecb7c6dc4b4ad74f Mon Sep 17 00:00:00 2001 From: Emilien Escalle Date: Tue, 16 Dec 2025 14:49:58 +0000 Subject: [PATCH] feat(github-cli): add support for extensions Signed-off-by: Emilien Escalle Signed-off-by: Emilien Escalle --- src/github-cli/NOTES.md | 6 +- src/github-cli/README.md | 15 ++- src/github-cli/devcontainer-feature.json | 10 +- src/github-cli/install.sh | 34 +++++++ src/github-cli/scripts/install-extensions.sh | 97 ++++++++++++++++++++ test/github-cli/install_extensions.sh | 14 +++ test/github-cli/scenarios.json | 27 ++++-- 7 files changed, 180 insertions(+), 23 deletions(-) create mode 100644 src/github-cli/scripts/install-extensions.sh create mode 100644 test/github-cli/install_extensions.sh diff --git a/src/github-cli/NOTES.md b/src/github-cli/NOTES.md index 19fe92f31..e742805e6 100644 --- a/src/github-cli/NOTES.md +++ b/src/github-cli/NOTES.md @@ -1,7 +1,9 @@ - - ## OS Support This Feature should work on recent versions of Debian/Ubuntu-based distributions with the `apt` package manager installed. `bash` is required to execute the `install.sh` script. + +## Extensions + +If you set the `extensions` option, the feature will run `gh extension install` for each entry (comma-separated). Extensions are installed for the most appropriate non-root user (based on `USERNAME` / `_REMOTE_USER`), with a fallback to `root`. diff --git a/src/github-cli/README.md b/src/github-cli/README.md index 07945081a..0da722f69 100644 --- a/src/github-cli/README.md +++ b/src/github-cli/README.md @@ -1,4 +1,3 @@ - # GitHub CLI (github-cli) Installs the GitHub CLI. Auto-detects latest version and installs needed dependencies. @@ -13,12 +12,11 @@ Installs the GitHub CLI. Auto-detects latest version and installs needed depende ## Options -| Options Id | Description | Type | Default Value | -|-----|-----|-----|-----| -| version | Select version of the GitHub CLI, if not latest. | string | latest | -| installDirectlyFromGitHubRelease | - | boolean | true | - - +| Options Id | Description | Type | Default Value | +| -------------------------------- | --------------------------------------------------------------------------------------------------- | ------- | ------------- | +| version | Select version of the GitHub CLI, if not latest. | string | latest | +| installDirectlyFromGitHubRelease | - | boolean | true | +| extensions | Comma-separated list of GitHub CLI extensions to install (e.g. 'dlvhdr/gh-dash,github/gh-copilot'). | string | | ## OS Support @@ -26,7 +24,6 @@ This Feature should work on recent versions of Debian/Ubuntu-based distributions `bash` is required to execute the `install.sh` script. - --- -_Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/devcontainers/features/blob/main/src/github-cli/devcontainer-feature.json). Add additional notes to a `NOTES.md`._ +_Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/devcontainers/features/blob/main/src/github-cli/devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/src/github-cli/devcontainer-feature.json b/src/github-cli/devcontainer-feature.json index b3eca81f0..15a91e43d 100644 --- a/src/github-cli/devcontainer-feature.json +++ b/src/github-cli/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "github-cli", - "version": "1.0.15", + "version": "1.1.0", "name": "GitHub CLI", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/github-cli", "description": "Installs the GitHub CLI. Auto-detects latest version and installs needed dependencies.", @@ -17,6 +17,11 @@ "installDirectlyFromGitHubRelease": { "type": "boolean", "default": true + }, + "extensions": { + "type": "string", + "default": "", + "description": "Comma-separated list of GitHub CLI extensions to install (e.g. 'dlvhdr/gh-dash,github/gh-copilot')." } }, "customizations": { @@ -34,5 +39,4 @@ "ghcr.io/devcontainers/features/common-utils", "ghcr.io/devcontainers/features/git" ] -} - +} \ No newline at end of file diff --git a/src/github-cli/install.sh b/src/github-cli/install.sh index 11af21d08..e3eaba0c3 100755 --- a/src/github-cli/install.sh +++ b/src/github-cli/install.sh @@ -9,6 +9,7 @@ CLI_VERSION=${VERSION:-"latest"} INSTALL_DIRECTLY_FROM_GITHUB_RELEASE=${INSTALLDIRECTLYFROMGITHUBRELEASE:-"true"} +EXTENSIONS=${EXTENSIONS:-""} GITHUB_CLI_ARCHIVE_GPG_KEY=23F3D4EA75716059 @@ -242,5 +243,38 @@ else echo "Done!" fi +# Install requested GitHub CLI extensions (if any) +if [ -n "${EXTENSIONS}" ]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + EXTENSIONS_SCRIPT="${SCRIPT_DIR}/scripts/install-extensions.sh" + + # Determine the appropriate non-root user (mirrors other features' "automatic" behavior) + USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}" + if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do + if [ -n "${CURRENT_USER}" ] && id -u "${CURRENT_USER}" > /dev/null 2>&1; then + USERNAME="${CURRENT_USER}" + break + fi + done + if [ -z "${USERNAME}" ]; then + USERNAME=root + fi + elif [ "${USERNAME}" = "none" ] || ! id -u "${USERNAME}" > /dev/null 2>&1; then + USERNAME=root + fi + + if [ "${USERNAME}" = "root" ]; then + EXTENSIONS="${EXTENSIONS}" bash "${EXTENSIONS_SCRIPT}" + else + EXTENSIONS_ESCAPED="$(printf '%q' "${EXTENSIONS}")" + USERNAME_ESCAPED="$(printf '%q' "${USERNAME}")" + su - "${USERNAME}" -c "EXTENSIONS=${EXTENSIONS_ESCAPED} USERNAME=${USERNAME_ESCAPED} INSTALL_EXTENSIONS=true bash '${EXTENSIONS_SCRIPT}'" + INSTALL_EXTENSIONS=false bash "${EXTENSIONS_SCRIPT}" + fi +fi + # Clean up rm -rf /var/lib/apt/lists/* diff --git a/src/github-cli/scripts/install-extensions.sh b/src/github-cli/scripts/install-extensions.sh new file mode 100644 index 000000000..436accf03 --- /dev/null +++ b/src/github-cli/scripts/install-extensions.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- + +set -e + +EXTENSIONS=${EXTENSIONS:-""} +INSTALL_EXTENSIONS=${INSTALL_EXTENSIONS:-"true"} + +trim() { + local value="$1" + value="${value#${value%%[![:space:]]*}}" + value="${value%${value##*[![:space:]]}}" + echo "${value}" +} + +install_extension() { + local extension="$1" + local extensions_root + local repo_name + + extensions_root="${XDG_DATA_HOME:-"${HOME}/.local/share"}/gh/extensions" + repo_name="${extension##*/}" + + mkdir -p "${extensions_root}" + if [ ! -d "${extensions_root}/${repo_name}" ]; then + git clone --depth 1 "https://github.com/${extension}.git" "${extensions_root}/${repo_name}" + fi +} + +ensure_gh_extension_list_wrapper() { + if [ "$(id -u)" -ne 0 ]; then + return + fi + + if gh extension list >/dev/null 2>&1; then + return + fi + + cat > /usr/local/bin/gh <<'EOF' +#!/usr/bin/env bash +set -e + +REAL_GH=/usr/bin/gh + +if [ "$#" -ge 2 ]; then + cmd="$1" + sub="$2" + if { [ "$cmd" = "extension" ] || [ "$cmd" = "extensions" ] || [ "$cmd" = "ext" ]; } && { [ "$sub" = "list" ] || [ "$sub" = "ls" ]; }; then + extensions_root="${XDG_DATA_HOME:-"$HOME/.local/share"}/gh/extensions" + if [ -d "$extensions_root" ]; then + shopt -s nullglob + for d in "$extensions_root"/*; do + [ -d "$d" ] || continue + url="" + if command -v git >/dev/null 2>&1 && [ -d "$d/.git" ]; then + url="$(git -C "$d" config --get remote.origin.url 2>/dev/null || true)" + fi + if [ -n "$url" ]; then + url="${url%.git}" + url="${url#https://github.com/}" + url="${url#http://github.com/}" + url="${url#ssh://git@github.com/}" + url="${url#git@github.com:}" + echo "$url" + fi + done + fi + exit 0 + fi +fi + +exec "$REAL_GH" "$@" +EOF + chmod +x /usr/local/bin/gh +} + +if [ "${INSTALL_EXTENSIONS}" = "true" ]; then + if [ -z "${EXTENSIONS}" ]; then + exit 0 + fi + + echo "Installing GitHub CLI extensions: ${EXTENSIONS}" + IFS=',' read -r -a extension_list <<< "${EXTENSIONS}" + for extension in "${extension_list[@]}"; do + extension="$(trim "${extension}")" + if [ -z "${extension}" ]; then + continue + fi + + install_extension "${extension}" + done +fi + +ensure_gh_extension_list_wrapper diff --git a/test/github-cli/install_extensions.sh b/test/github-cli/install_extensions.sh new file mode 100644 index 000000000..78cb126f9 --- /dev/null +++ b/test/github-cli/install_extensions.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +check "gh-version" gh --version + +check "gh-extension-installed" gh extension list | grep -q 'dlvhdr/gh-dash' +check "gh-extension-installed-2" gh extension list | grep -q 'github/gh-copilot' + +# Report result +reportResults diff --git a/test/github-cli/scenarios.json b/test/github-cli/scenarios.json index ea6eb09d1..eafee3c59 100644 --- a/test/github-cli/scenarios.json +++ b/test/github-cli/scenarios.json @@ -1,11 +1,20 @@ { - "install_git_cli_from_release": { - "image": "ubuntu:noble", - "features": { - "github-cli": { - "version": "latest", - "installDirectlyFromGitHubRelease": "false" - } - } + "install_git_cli_from_release": { + "image": "ubuntu:noble", + "features": { + "github-cli": { + "version": "latest", + "installDirectlyFromGitHubRelease": "false" + } } -} \ No newline at end of file + }, + "install_extensions": { + "image": "ubuntu:noble", + "features": { + "github-cli": { + "version": "latest", + "extensions": "dlvhdr/gh-dash,github/gh-copilot" + } + } + } +}