Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/version-resolution.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: "CI - Version Resolution Fallbacks"

on:
push:
branches:
- main
paths:
- ".github/workflows/version-resolution.yaml"
- "scripts/version-resolution.sh"
- "scripts/sync-version-resolution.sh"
- "src/**/install.sh"
- "src/**/scripts/version-resolution.sh"
- "test/_global/version-resolution/**"
pull_request:
paths:
- ".github/workflows/version-resolution.yaml"
- "scripts/version-resolution.sh"
- "scripts/sync-version-resolution.sh"
- "src/**/install.sh"
- "src/**/scripts/version-resolution.sh"
- "test/_global/version-resolution/**"

jobs:
version-resolution:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Check synchronized resolver copies
run: bash scripts/sync-version-resolution.sh --check

- name: Test fallback resolution
run: bash test/_global/version-resolution/test.sh

- name: Lint resolver scripts
run: |
shellcheck -e SC2072 \
scripts/version-resolution.sh \
scripts/sync-version-resolution.sh \
test/_global/version-resolution/test.sh \
test/_global/version-resolution/cases.sh
40 changes: 40 additions & 0 deletions scripts/sync-version-resolution.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SOURCE_FILE="${ROOT_DIR}/scripts/version-resolution.sh"
FEATURES=(
copilot-cli
docker-in-docker
docker-outside-of-docker
git-lfs
github-cli
go
kubectl-helm-minikube
nix
node
php
powershell
python
rust
terraform
)

check_only=false
if [ "${1:-}" = "--check" ]; then
check_only=true
fi

for feature in "${FEATURES[@]}"; do
target_dir="${ROOT_DIR}/src/${feature}/scripts"
target_file="${target_dir}/version-resolution.sh"
if ${check_only}; then
if ! cmp -s "${SOURCE_FILE}" "${target_file}"; then
echo "${target_file} is not synchronized with ${SOURCE_FILE}." >&2
exit 1
fi
else
mkdir -p "${target_dir}"
cp "${SOURCE_FILE}" "${target_file}"
fi
done
140 changes: 140 additions & 0 deletions scripts/version-resolution.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env bash

# Resolve version requests from git tags, with a caller-provided fallback resolver.
# The fallback function must print candidate versions, one per line.
find_version_from_git_tags() {
local variable_name=$1
local requested_version=${!variable_name}
local repository=$2
local prefix=${3:-"tags/v"}
local separator=${4:-"."}
local last_part_optional=${5:-"false"}
local version_suffix_regex=${6:-""}
local known_good_version=${7:-""}
local fallback_name=${8:-""}
local fallback_function=${9:-""}

if [ "${requested_version}" = "none" ]; then
return
fi

local tag_prefix=${prefix#refs/}
tag_prefix=${tag_prefix#tags/}
local normalized_request=${requested_version#"${tag_prefix}"}

local escaped_separator=${separator//./\.}
local patch_regex="${escaped_separator}[0-9]+"
if [ "${last_part_optional}" = "true" ]; then
patch_regex="(${patch_regex})?"
fi
local version_regex="[0-9]+${escaped_separator}[0-9]+${patch_regex}${version_suffix_regex//./\.}"

# Fully qualified versions are user assertions. Do not require the network
# to prove that a requested release exists.
if echo "${normalized_request}" | grep -Eq "^${version_regex}$"; then
declare -g "${variable_name}=${normalized_request}"
echo "${variable_name}=${normalized_request}"
return
fi

local version_list=""
local git_output=""
if git_output="$(git ls-remote --tags "${repository}" 2>/dev/null)"; then
version_list="$(_extract_version_candidates "${git_output}" "${prefix}" "${separator}" "${version_regex}")"
fi

if [ -z "${version_list}" ]; then
echo "(!) Unable to resolve '${requested_version}' from git tags at ${repository}." >&2
if [ -z "${fallback_function}" ] && echo "${repository}" | grep -qE '^https://github.com/[^/]+/[^/]+/?$'; then
fallback_name="GitHub REST API"
fallback_function="_github_rest_version_candidates"
fi
if [ -n "${fallback_function}" ]; then
echo "(*) Trying ${fallback_name:-alternate version source}." >&2
local fallback_output=""
if fallback_output="$(${fallback_function} "${repository}" "${prefix}" 2>/dev/null)"; then
version_list="$(_normalize_version_candidates "${fallback_output}" "${separator}" "${version_regex}")"
fi
if [ -n "${version_list}" ]; then
echo "(*) Resolved version using ${fallback_name:-alternate version source}." >&2
fi
fi
fi

local resolved_version=""
resolved_version="$(_select_requested_version "${normalized_request}" "${version_list}")"
if [ -z "${resolved_version}" ] && [ -n "${known_good_version}" ] && _version_matches_request "${normalized_request}" "${known_good_version}"; then
resolved_version="${known_good_version}"
echo "(!) Dynamic version resolution failed; using known-good version ${known_good_version}." >&2
fi

if [ -z "${resolved_version}" ]; then
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
return 1
fi

declare -g "${variable_name}=${resolved_version}"
echo "${variable_name}=${resolved_version}"
}

_github_rest_version_candidates() {
local repository=$1
local prefix=${2:-"tags/v"}
local slug=${repository#https://github.com/}
slug=${slug%/}
local tag_prefix=${prefix#refs/}
tag_prefix=${tag_prefix#tags/}

curl -fsSL "https://api.github.com/repos/${slug}/tags?per_page=100" \
| grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"]+"' \
| sed -E 's/^.*"([^"]+)"$/\1/' \
| sed "s|^${tag_prefix}||"
}

_extract_version_candidates() {
local input=$1
local prefix=$2
local separator=$3
local version_regex=$4
local escaped_prefix=${prefix//./\.}

printf '%s\n' "${input}" \
| grep -oE "${escaped_prefix}${version_regex}$" \
| sed "s|^${prefix}||" \
| tr "${separator}" "." \
| sort -rVu || true
}

_normalize_version_candidates() {
local input=$1
local separator=$2
local version_regex=$3

printf '%s\n' "${input}" \
| grep -oE "^${version_regex}$" \
| tr "${separator}" "." \
| sort -rVu || true
}

_select_requested_version() {
local requested_version=$1
local version_list=$2

if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "stable" ] || [ "${requested_version}" = "lts" ] || [ "${requested_version}" = "prerelease" ] || [ "${requested_version}" = "preview" ]; then
printf '%s\n' "${version_list}" | head -n 1
return
fi

printf '%s\n' "${version_list}" | grep -E -m 1 "^${requested_version//./\.}([.]|[-]|$)" || true
}

_version_matches_request() {
local requested_version=$1
local candidate=$2

if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "stable" ] || [ "${requested_version}" = "lts" ] || [ "${requested_version}" = "prerelease" ] || [ "${requested_version}" = "preview" ]; then
return 0
fi

echo "${candidate}" | grep -Eq "^${requested_version//./\.}([.]|[-]|$)"
}
2 changes: 1 addition & 1 deletion src/copilot-cli/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "copilot-cli",
"version": "1.1.3",
"version": "1.1.4",
"name": "GitHub Copilot CLI",
"documentationURL": "https://github.com/devcontainers/features/tree/main/src/copilot-cli",
"description": "Installs the GitHub Copilot CLI.",
Expand Down
17 changes: 6 additions & 11 deletions src/copilot-cli/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
# Maintainer: The VS Code and Codespaces Teams

CLI_VERSION=${VERSION:-"latest"}
REQUESTED_CLI_VERSION="${CLI_VERSION}"
COPILOT_CLI_LAST_KNOWN_VERSION="1.0.87-0"

set -e

Expand All @@ -31,13 +33,7 @@ check_packages() {
fi
}

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

download_from_github() {
local release_url=$1
Expand Down Expand Up @@ -71,9 +67,8 @@ install_using_github() {
if [ "${CLI_VERSION}" = "latest" ]; then
download_from_github "https://github.com/github/copilot-cli/releases/latest/download/${cli_filename}"
elif [ "${CLI_VERSION}" = "prerelease" ]; then

prerelease_version="$(resolve_prerelease_version "$(git ls-remote --tags https://github.com/github/copilot-cli)")"
download_from_github "https://github.com/github/copilot-cli/releases/download/${prerelease_version}/${cli_filename}"
find_version_from_git_tags CLI_VERSION "https://github.com/github/copilot-cli" "tags/v" "." "false" "(-[0-9]+)" "${COPILOT_CLI_LAST_KNOWN_VERSION}"
download_from_github "https://github.com/github/copilot-cli/releases/download/v${CLI_VERSION}/${cli_filename}"

else
# Install specific version
Expand All @@ -91,7 +86,7 @@ echo "Downloading GitHub Copilot CLI..."
install_using_github

# Create a flag file if using "latest" or "prerelease" so the postStartCommand knows to auto-update
if [ "${CLI_VERSION}" = "latest" ] || [ "${CLI_VERSION}" = "prerelease" ]; then
if [ "${REQUESTED_CLI_VERSION}" = "latest" ] || [ "${REQUESTED_CLI_VERSION}" = "prerelease" ]; then
mkdir -p /etc/devcontainer-copilot-cli
touch /etc/devcontainer-copilot-cli/auto-update
fi
Expand Down
Loading
Loading