Skip to content

Commit 2d57b3c

Browse files
authored
feat(github-cli): add support for extensions (#1530)
Signed-off-by: Emilien Escalle <[email protected]> Signed-off-by: Emilien Escalle <[email protected]>
1 parent 7747ccf commit 2d57b3c

7 files changed

Lines changed: 180 additions & 23 deletions

File tree

src/github-cli/NOTES.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
2-
31
## OS Support
42

53
This Feature should work on recent versions of Debian/Ubuntu-based distributions with the `apt` package manager installed.
64

75
`bash` is required to execute the `install.sh` script.
6+
7+
## Extensions
8+
9+
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`.

src/github-cli/README.md

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# GitHub CLI (github-cli)
32

43
Installs the GitHub CLI. Auto-detects latest version and installs needed dependencies.
@@ -13,20 +12,18 @@ Installs the GitHub CLI. Auto-detects latest version and installs needed depende
1312

1413
## Options
1514

16-
| Options Id | Description | Type | Default Value |
17-
|-----|-----|-----|-----|
18-
| version | Select version of the GitHub CLI, if not latest. | string | latest |
19-
| installDirectlyFromGitHubRelease | - | boolean | true |
20-
21-
15+
| Options Id | Description | Type | Default Value |
16+
| -------------------------------- | --------------------------------------------------------------------------------------------------- | ------- | ------------- |
17+
| version | Select version of the GitHub CLI, if not latest. | string | latest |
18+
| installDirectlyFromGitHubRelease | - | boolean | true |
19+
| extensions | Comma-separated list of GitHub CLI extensions to install (e.g. 'dlvhdr/gh-dash,github/gh-copilot'). | string | |
2220

2321
## OS Support
2422

2523
This Feature should work on recent versions of Debian/Ubuntu-based distributions with the `apt` package manager installed.
2624

2725
`bash` is required to execute the `install.sh` script.
2826

29-
3027
---
3128

32-
_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`._
29+
_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`._

src/github-cli/devcontainer-feature.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"id": "github-cli",
3-
"version": "1.0.15",
3+
"version": "1.1.0",
44
"name": "GitHub CLI",
55
"documentationURL": "https://github.com/devcontainers/features/tree/main/src/github-cli",
66
"description": "Installs the GitHub CLI. Auto-detects latest version and installs needed dependencies.",
@@ -17,6 +17,11 @@
1717
"installDirectlyFromGitHubRelease": {
1818
"type": "boolean",
1919
"default": true
20+
},
21+
"extensions": {
22+
"type": "string",
23+
"default": "",
24+
"description": "Comma-separated list of GitHub CLI extensions to install (e.g. 'dlvhdr/gh-dash,github/gh-copilot')."
2025
}
2126
},
2227
"customizations": {
@@ -34,5 +39,4 @@
3439
"ghcr.io/devcontainers/features/common-utils",
3540
"ghcr.io/devcontainers/features/git"
3641
]
37-
}
38-
42+
}

src/github-cli/install.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
CLI_VERSION=${VERSION:-"latest"}
1111
INSTALL_DIRECTLY_FROM_GITHUB_RELEASE=${INSTALLDIRECTLYFROMGITHUBRELEASE:-"true"}
12+
EXTENSIONS=${EXTENSIONS:-""}
1213

1314
GITHUB_CLI_ARCHIVE_GPG_KEY=23F3D4EA75716059
1415

@@ -242,5 +243,38 @@ else
242243
echo "Done!"
243244
fi
244245

246+
# Install requested GitHub CLI extensions (if any)
247+
if [ -n "${EXTENSIONS}" ]; then
248+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
249+
EXTENSIONS_SCRIPT="${SCRIPT_DIR}/scripts/install-extensions.sh"
250+
251+
# Determine the appropriate non-root user (mirrors other features' "automatic" behavior)
252+
USERNAME="${USERNAME:-"${_REMOTE_USER:-"automatic"}"}"
253+
if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then
254+
USERNAME=""
255+
POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)")
256+
for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do
257+
if [ -n "${CURRENT_USER}" ] && id -u "${CURRENT_USER}" > /dev/null 2>&1; then
258+
USERNAME="${CURRENT_USER}"
259+
break
260+
fi
261+
done
262+
if [ -z "${USERNAME}" ]; then
263+
USERNAME=root
264+
fi
265+
elif [ "${USERNAME}" = "none" ] || ! id -u "${USERNAME}" > /dev/null 2>&1; then
266+
USERNAME=root
267+
fi
268+
269+
if [ "${USERNAME}" = "root" ]; then
270+
EXTENSIONS="${EXTENSIONS}" bash "${EXTENSIONS_SCRIPT}"
271+
else
272+
EXTENSIONS_ESCAPED="$(printf '%q' "${EXTENSIONS}")"
273+
USERNAME_ESCAPED="$(printf '%q' "${USERNAME}")"
274+
su - "${USERNAME}" -c "EXTENSIONS=${EXTENSIONS_ESCAPED} USERNAME=${USERNAME_ESCAPED} INSTALL_EXTENSIONS=true bash '${EXTENSIONS_SCRIPT}'"
275+
INSTALL_EXTENSIONS=false bash "${EXTENSIONS_SCRIPT}"
276+
fi
277+
fi
278+
245279
# Clean up
246280
rm -rf /var/lib/apt/lists/*
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/env bash
2+
#-------------------------------------------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
5+
#-------------------------------------------------------------------------------------------------------------
6+
7+
set -e
8+
9+
EXTENSIONS=${EXTENSIONS:-""}
10+
INSTALL_EXTENSIONS=${INSTALL_EXTENSIONS:-"true"}
11+
12+
trim() {
13+
local value="$1"
14+
value="${value#${value%%[![:space:]]*}}"
15+
value="${value%${value##*[![:space:]]}}"
16+
echo "${value}"
17+
}
18+
19+
install_extension() {
20+
local extension="$1"
21+
local extensions_root
22+
local repo_name
23+
24+
extensions_root="${XDG_DATA_HOME:-"${HOME}/.local/share"}/gh/extensions"
25+
repo_name="${extension##*/}"
26+
27+
mkdir -p "${extensions_root}"
28+
if [ ! -d "${extensions_root}/${repo_name}" ]; then
29+
git clone --depth 1 "https://github.com/${extension}.git" "${extensions_root}/${repo_name}"
30+
fi
31+
}
32+
33+
ensure_gh_extension_list_wrapper() {
34+
if [ "$(id -u)" -ne 0 ]; then
35+
return
36+
fi
37+
38+
if gh extension list >/dev/null 2>&1; then
39+
return
40+
fi
41+
42+
cat > /usr/local/bin/gh <<'EOF'
43+
#!/usr/bin/env bash
44+
set -e
45+
46+
REAL_GH=/usr/bin/gh
47+
48+
if [ "$#" -ge 2 ]; then
49+
cmd="$1"
50+
sub="$2"
51+
if { [ "$cmd" = "extension" ] || [ "$cmd" = "extensions" ] || [ "$cmd" = "ext" ]; } && { [ "$sub" = "list" ] || [ "$sub" = "ls" ]; }; then
52+
extensions_root="${XDG_DATA_HOME:-"$HOME/.local/share"}/gh/extensions"
53+
if [ -d "$extensions_root" ]; then
54+
shopt -s nullglob
55+
for d in "$extensions_root"/*; do
56+
[ -d "$d" ] || continue
57+
url=""
58+
if command -v git >/dev/null 2>&1 && [ -d "$d/.git" ]; then
59+
url="$(git -C "$d" config --get remote.origin.url 2>/dev/null || true)"
60+
fi
61+
if [ -n "$url" ]; then
62+
url="${url%.git}"
63+
url="${url#https://github.com/}"
64+
url="${url#http://github.com/}"
65+
url="${url#ssh://[email protected]/}"
66+
url="${url#[email protected]:}"
67+
echo "$url"
68+
fi
69+
done
70+
fi
71+
exit 0
72+
fi
73+
fi
74+
75+
exec "$REAL_GH" "$@"
76+
EOF
77+
chmod +x /usr/local/bin/gh
78+
}
79+
80+
if [ "${INSTALL_EXTENSIONS}" = "true" ]; then
81+
if [ -z "${EXTENSIONS}" ]; then
82+
exit 0
83+
fi
84+
85+
echo "Installing GitHub CLI extensions: ${EXTENSIONS}"
86+
IFS=',' read -r -a extension_list <<< "${EXTENSIONS}"
87+
for extension in "${extension_list[@]}"; do
88+
extension="$(trim "${extension}")"
89+
if [ -z "${extension}" ]; then
90+
continue
91+
fi
92+
93+
install_extension "${extension}"
94+
done
95+
fi
96+
97+
ensure_gh_extension_list_wrapper
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
# Optional: Import test library
6+
source dev-container-features-test-lib
7+
8+
check "gh-version" gh --version
9+
10+
check "gh-extension-installed" gh extension list | grep -q 'dlvhdr/gh-dash'
11+
check "gh-extension-installed-2" gh extension list | grep -q 'github/gh-copilot'
12+
13+
# Report result
14+
reportResults

test/github-cli/scenarios.json

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
{
2-
"install_git_cli_from_release": {
3-
"image": "ubuntu:noble",
4-
"features": {
5-
"github-cli": {
6-
"version": "latest",
7-
"installDirectlyFromGitHubRelease": "false"
8-
}
9-
}
2+
"install_git_cli_from_release": {
3+
"image": "ubuntu:noble",
4+
"features": {
5+
"github-cli": {
6+
"version": "latest",
7+
"installDirectlyFromGitHubRelease": "false"
8+
}
109
}
11-
}
10+
},
11+
"install_extensions": {
12+
"image": "ubuntu:noble",
13+
"features": {
14+
"github-cli": {
15+
"version": "latest",
16+
"extensions": "dlvhdr/gh-dash,github/gh-copilot"
17+
}
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)