Skip to content

Commit fbb4442

Browse files
authored
Fallback for version resolution over unauthenticated git for feature installation. (#1744)
* Fallback for version resolution over unauthenticated git for feature installation. * Fixing test issues and version resolution logic * Further changes for better maintainability * Modify the test script * Implementing review comments * Adding further review comment fixes.
1 parent 7893e53 commit fbb4442

42 files changed

Lines changed: 2689 additions & 52 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: "CI - Version Resolution Fallbacks"
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- ".github/workflows/version-resolution.yaml"
9+
- "scripts/version-resolution.sh"
10+
- "scripts/sync-version-resolution.sh"
11+
- "src/**/install.sh"
12+
- "src/**/scripts/version-resolution.sh"
13+
- "test/_global/version-resolution/**"
14+
pull_request:
15+
paths:
16+
- ".github/workflows/version-resolution.yaml"
17+
- "scripts/version-resolution.sh"
18+
- "scripts/sync-version-resolution.sh"
19+
- "src/**/install.sh"
20+
- "src/**/scripts/version-resolution.sh"
21+
- "test/_global/version-resolution/**"
22+
23+
jobs:
24+
version-resolution:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v7
28+
29+
- name: Check synchronized resolver copies
30+
run: bash scripts/sync-version-resolution.sh --check
31+
32+
- name: Test fallback resolution
33+
run: bash test/_global/version-resolution/test.sh
34+
35+
- name: Lint resolver scripts
36+
run: |
37+
shellcheck -e SC2072 \
38+
scripts/version-resolution.sh \
39+
scripts/sync-version-resolution.sh \
40+
test/_global/version-resolution/test.sh \
41+
test/_global/version-resolution/cases.sh

‎scripts/sync-version-resolution.sh‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5+
SOURCE_FILE="${ROOT_DIR}/scripts/version-resolution.sh"
6+
FEATURES=(
7+
copilot-cli
8+
docker-in-docker
9+
docker-outside-of-docker
10+
git-lfs
11+
github-cli
12+
go
13+
kubectl-helm-minikube
14+
nix
15+
node
16+
php
17+
powershell
18+
python
19+
rust
20+
terraform
21+
)
22+
23+
check_only=false
24+
if [ "${1:-}" = "--check" ]; then
25+
check_only=true
26+
fi
27+
28+
for feature in "${FEATURES[@]}"; do
29+
target_dir="${ROOT_DIR}/src/${feature}/scripts"
30+
target_file="${target_dir}/version-resolution.sh"
31+
if ${check_only}; then
32+
if ! cmp -s "${SOURCE_FILE}" "${target_file}"; then
33+
echo "${target_file} is not synchronized with ${SOURCE_FILE}." >&2
34+
exit 1
35+
fi
36+
else
37+
mkdir -p "${target_dir}"
38+
cp "${SOURCE_FILE}" "${target_file}"
39+
fi
40+
done

‎scripts/version-resolution.sh‎

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/usr/bin/env bash
2+
3+
# Resolve version requests from git tags, with a caller-provided fallback resolver.
4+
# The fallback function must print candidate versions, one per line.
5+
find_version_from_git_tags() {
6+
local variable_name=$1
7+
local requested_version=${!variable_name}
8+
local repository=$2
9+
local prefix=${3:-"tags/v"}
10+
local separator=${4:-"."}
11+
local last_part_optional=${5:-"false"}
12+
local version_suffix_regex=${6:-""}
13+
local known_good_version=${7:-""}
14+
local fallback_name=${8:-""}
15+
local fallback_function=${9:-""}
16+
17+
if [ "${requested_version}" = "none" ]; then
18+
return
19+
fi
20+
21+
local tag_prefix=${prefix#refs/}
22+
tag_prefix=${tag_prefix#tags/}
23+
local normalized_request=${requested_version#"${tag_prefix}"}
24+
25+
local escaped_separator=${separator//./\\.}
26+
local patch_regex="${escaped_separator}[0-9]+"
27+
local exact_version_regex="[0-9]+${escaped_separator}[0-9]+${patch_regex}${version_suffix_regex//./\.}"
28+
if [ "${last_part_optional}" = "true" ]; then
29+
patch_regex="(${patch_regex})?"
30+
fi
31+
local version_regex="[0-9]+${escaped_separator}[0-9]+${patch_regex}${version_suffix_regex//./\.}"
32+
33+
# Only three-component versions are exact assertions; optional-patch tags
34+
# remain candidates when resolving partial requests such as Go's major.minor.
35+
if echo "${normalized_request}" | grep -Eq "^${exact_version_regex}$"; then
36+
declare -g "${variable_name}=${normalized_request}"
37+
echo "${variable_name}=${normalized_request}"
38+
return
39+
fi
40+
41+
local version_list=""
42+
local git_output=""
43+
if git_output="$(git ls-remote --tags "${repository}" 2>/dev/null)"; then
44+
version_list="$(_extract_version_candidates "${git_output}" "${prefix}" "${separator}" "${version_regex}")"
45+
fi
46+
47+
if [ -z "${version_list}" ]; then
48+
echo "(!) Unable to resolve '${requested_version}' from git tags at ${repository}." >&2
49+
if [ -z "${fallback_function}" ] && echo "${repository}" | grep -qE '^https://github.com/[^/]+/[^/]+/?$'; then
50+
fallback_name="GitHub REST API"
51+
fallback_function="_github_rest_version_candidates"
52+
fi
53+
if [ -n "${fallback_function}" ]; then
54+
echo "(*) Trying ${fallback_name:-alternate version source}." >&2
55+
local fallback_output=""
56+
if fallback_output="$(${fallback_function} "${repository}" "${prefix}" 2>/dev/null)"; then
57+
version_list="$(_normalize_version_candidates "${fallback_output}" "${separator}" "${version_regex}")"
58+
fi
59+
if [ -n "${version_list}" ]; then
60+
echo "(*) Resolved version using ${fallback_name:-alternate version source}." >&2
61+
fi
62+
fi
63+
fi
64+
65+
local resolved_version=""
66+
resolved_version="$(_select_requested_version "${normalized_request}" "${version_list}")"
67+
if [ -z "${resolved_version}" ] && [ -n "${known_good_version}" ] && _version_matches_request "${normalized_request}" "${known_good_version}"; then
68+
resolved_version="${known_good_version}"
69+
echo "(!) Dynamic version resolution failed; using known-good version ${known_good_version}." >&2
70+
fi
71+
72+
if [ -z "${resolved_version}" ]; then
73+
echo "(!) Unable to resolve '${requested_version}' for ${variable_name}. Tried git tags at ${repository}${fallback_name:+ and ${fallback_name}}. Specify an exact version to avoid remote resolution." >&2
74+
return 1
75+
fi
76+
77+
declare -g "${variable_name}=${resolved_version}"
78+
echo "${variable_name}=${resolved_version}"
79+
}
80+
81+
_github_rest_version_candidates() {
82+
local repository=$1
83+
local prefix=${2:-"tags/v"}
84+
local slug=${repository#https://github.com/}
85+
slug=${slug%/}
86+
local tag_prefix=${prefix#refs/}
87+
tag_prefix=${tag_prefix#tags/}
88+
89+
curl -fsSL "https://api.github.com/repos/${slug}/tags?per_page=100" \
90+
| grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"]+"' \
91+
| sed -E 's/^.*"([^"]+)"$/\1/' \
92+
| sed "s|^${tag_prefix}||"
93+
}
94+
95+
_extract_version_candidates() {
96+
local input=$1
97+
local prefix=$2
98+
local separator=$3
99+
local version_regex=$4
100+
local escaped_prefix=${prefix//./\.}
101+
102+
printf '%s\n' "${input}" \
103+
| grep -oE "${escaped_prefix}${version_regex}$" \
104+
| sed "s|^${prefix}||" \
105+
| tr "${separator}" "." \
106+
| sort -rVu || true
107+
}
108+
109+
_normalize_version_candidates() {
110+
local input=$1
111+
local separator=$2
112+
local version_regex=$3
113+
114+
printf '%s\n' "${input}" \
115+
| grep -oE "^${version_regex}$" \
116+
| tr "${separator}" "." \
117+
| sort -rVu || true
118+
}
119+
120+
_select_requested_version() {
121+
local requested_version=$1
122+
local version_list=$2
123+
124+
if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "stable" ] || [ "${requested_version}" = "lts" ] || [ "${requested_version}" = "prerelease" ] || [ "${requested_version}" = "preview" ]; then
125+
printf '%s\n' "${version_list}" | head -n 1
126+
return
127+
fi
128+
129+
printf '%s\n' "${version_list}" | grep -E -m 1 "^${requested_version//./\.}([.]|[-]|$)" || true
130+
}
131+
132+
_version_matches_request() {
133+
local requested_version=$1
134+
local candidate=$2
135+
136+
if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "stable" ] || [ "${requested_version}" = "lts" ] || [ "${requested_version}" = "prerelease" ] || [ "${requested_version}" = "preview" ]; then
137+
return 0
138+
fi
139+
140+
echo "${candidate}" | grep -Eq "^${requested_version//./\.}([.]|[-]|$)"
141+
}

‎src/copilot-cli/devcontainer-feature.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"id": "copilot-cli",
3-
"version": "1.1.3",
3+
"version": "1.1.4",
44
"name": "GitHub Copilot CLI",
55
"documentationURL": "https://github.com/devcontainers/features/tree/main/src/copilot-cli",
66
"description": "Installs the GitHub Copilot CLI.",

‎src/copilot-cli/install.sh‎

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
# Maintainer: The VS Code and Codespaces Teams
99

1010
CLI_VERSION=${VERSION:-"latest"}
11+
REQUESTED_CLI_VERSION="${CLI_VERSION}"
12+
COPILOT_CLI_LAST_KNOWN_VERSION="1.0.87-0"
1113

1214
set -e
1315

@@ -31,13 +33,7 @@ check_packages() {
3133
fi
3234
}
3335

34-
resolve_prerelease_version() {
35-
local repo_versions="${1:?resolve_prerelease_version requires the copilot-cli repo tags as input}"
36-
printf '%s\n' "${repo_versions}" \
37-
| awk '{print $2}' | sed 's|refs/tags/||' \
38-
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$' \
39-
| sort -V | tail -n1
40-
}
36+
. "$(dirname "${BASH_SOURCE[0]}")/scripts/version-resolution.sh"
4137

4238
download_from_github() {
4339
local release_url=$1
@@ -54,7 +50,7 @@ download_from_github() {
5450
}
5551

5652
install_using_github() {
57-
check_packages wget tar ca-certificates git
53+
check_packages wget curl tar ca-certificates git
5854
echo "Finished setting up dependencies"
5955
arch=$(dpkg --print-architecture)
6056
if [ "${arch}" = "amd64" ]; then
@@ -71,9 +67,8 @@ install_using_github() {
7167
if [ "${CLI_VERSION}" = "latest" ]; then
7268
download_from_github "https://github.com/github/copilot-cli/releases/latest/download/${cli_filename}"
7369
elif [ "${CLI_VERSION}" = "prerelease" ]; then
74-
75-
prerelease_version="$(resolve_prerelease_version "$(git ls-remote --tags https://github.com/github/copilot-cli)")"
76-
download_from_github "https://github.com/github/copilot-cli/releases/download/${prerelease_version}/${cli_filename}"
70+
find_version_from_git_tags CLI_VERSION "https://github.com/github/copilot-cli" "tags/v" "." "false" "(-[0-9]+)?" "${COPILOT_CLI_LAST_KNOWN_VERSION}"
71+
download_from_github "https://github.com/github/copilot-cli/releases/download/v${CLI_VERSION}/${cli_filename}"
7772

7873
else
7974
# Install specific version
@@ -91,8 +86,7 @@ echo "Downloading GitHub Copilot CLI..."
9186
install_using_github
9287

9388
# Create a flag file if using "latest" or "prerelease" so the postStartCommand knows to auto-update
94-
if [ "${CLI_VERSION}" = "latest" ] || [ "${CLI_VERSION}" = "prerelease" ]; then
89+
if [ "${REQUESTED_CLI_VERSION}" = "latest" ] || [ "${REQUESTED_CLI_VERSION}" = "prerelease" ]; then
9590
mkdir -p /etc/devcontainer-copilot-cli
9691
touch /etc/devcontainer-copilot-cli/auto-update
9792
fi
98-

0 commit comments

Comments
 (0)