From aceee796f175d5b4fc2741cd359038ef3bcf1cb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 15:51:51 +0000 Subject: [PATCH 1/4] Initial plan From 63f4f148ae1cd0eeebca6bcfcc932349340bd90d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 15:54:41 +0000 Subject: [PATCH 2/4] fix(copilot-cli): use semver sort for prerelease tag resolution --- src/copilot-cli/devcontainer-feature.json | 2 +- src/copilot-cli/install.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/copilot-cli/devcontainer-feature.json b/src/copilot-cli/devcontainer-feature.json index bfd97286c..f4db33ced 100644 --- a/src/copilot-cli/devcontainer-feature.json +++ b/src/copilot-cli/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "copilot-cli", - "version": "1.1.2", + "version": "1.1.3", "name": "GitHub Copilot CLI", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/copilot-cli", "description": "Installs the GitHub Copilot CLI.", diff --git a/src/copilot-cli/install.sh b/src/copilot-cli/install.sh index 47c2e3aca..0b91ee0aa 100755 --- a/src/copilot-cli/install.sh +++ b/src/copilot-cli/install.sh @@ -63,7 +63,7 @@ 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="$(git ls-remote --tags https://github.com/github/copilot-cli | tail -1 | awk -F/ '{print $NF}')" + prerelease_version="$(git ls-remote --tags https://github.com/github/copilot-cli | awk '{print $2}' | sed 's|refs/tags/||' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?' | sort -V | tail -n1)" download_from_github "https://github.com/github/copilot-cli/releases/download/${prerelease_version}/${cli_filename}" else # Install specific version From e38d64bac98b9d34d919484edd052f2812949aa8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 11:24:09 +0000 Subject: [PATCH 3/4] refactor(copilot-cli): extract resolve_prerelease_version function and add test Extract the prerelease tag resolution logic into a standalone resolve_prerelease_version() function that can read from stdin for testing. Add a scenario test that validates version sorting with mock git ls-remote data to prevent regressions. --- src/copilot-cli/install.sh | 17 +++++- .../copilot-cli/resolve_prerelease_version.sh | 57 +++++++++++++++++++ test/copilot-cli/scenarios.json | 10 ++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 test/copilot-cli/resolve_prerelease_version.sh create mode 100644 test/copilot-cli/scenarios.json diff --git a/src/copilot-cli/install.sh b/src/copilot-cli/install.sh index 0b91ee0aa..519142f28 100755 --- a/src/copilot-cli/install.sh +++ b/src/copilot-cli/install.sh @@ -45,6 +45,21 @@ download_from_github() { rm -rf /tmp/copilotcli } +# Resolves the latest prerelease version tag from git ls-remote output. +# Filters to well-formed vX.Y.Z or vX.Y.Z-N tags and sorts by version. +# Reads ls-remote formatted lines from stdin if no arguments are provided, +# otherwise runs git ls-remote --tags against the given repository URL. +resolve_prerelease_version() { + local repo_url="${1:-}" + if [ -n "${repo_url}" ]; then + git ls-remote --tags "${repo_url}" + else + cat + fi | awk '{print $2}' | sed 's|refs/tags/||' \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$' \ + | sort -V | tail -n1 +} + install_using_github() { check_packages wget tar ca-certificates git echo "Finished setting up dependencies" @@ -63,7 +78,7 @@ 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="$(git ls-remote --tags https://github.com/github/copilot-cli | awk '{print $2}' | sed 's|refs/tags/||' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?' | sort -V | tail -n1)" + prerelease_version="$(resolve_prerelease_version "https://github.com/github/copilot-cli")" download_from_github "https://github.com/github/copilot-cli/releases/download/${prerelease_version}/${cli_filename}" else # Install specific version diff --git a/test/copilot-cli/resolve_prerelease_version.sh b/test/copilot-cli/resolve_prerelease_version.sh new file mode 100644 index 000000000..1ff182a2b --- /dev/null +++ b/test/copilot-cli/resolve_prerelease_version.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +set -e + +# Optional: Import test library +source dev-container-features-test-lib + +# Source the install script's resolve_prerelease_version function +# We extract it by sourcing, but we only need the function definition +eval "$(sed -n '/^resolve_prerelease_version()/,/^}/p' /usr/local/share/copilot-cli-install.sh 2>/dev/null || true)" + +# If sourcing didn't work, define it inline (matches src/copilot-cli/install.sh) +if ! type resolve_prerelease_version &>/dev/null; then + resolve_prerelease_version() { + local repo_url="${1:-}" + if [ -n "${repo_url}" ]; then + git ls-remote --tags "${repo_url}" + else + cat + fi | awk '{print $2}' | sed 's|refs/tags/||' \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$' \ + | sort -V | tail -n1 + } +fi + +# Mock git ls-remote output (alphabetically v1.0.9 > v1.0.45) +MOCK_LS_REMOTE="abc1234\trefs/tags/v1.0.1 +def5678\trefs/tags/v1.0.9 +ghi9012\trefs/tags/v1.0.10 +jkl3456\trefs/tags/v1.0.45 +mno7890\trefs/tags/v1.0.2" + +# Test: version sort picks v1.0.45, not v1.0.9 +result="$(echo "${MOCK_LS_REMOTE}" | resolve_prerelease_version)" +check "picks highest version (v1.0.45)" bash -c "[ '${result}' = 'v1.0.45' ]" + +# Test: prerelease numeric suffixes sort correctly +MOCK_PRERELEASE="abc1234\trefs/tags/v1.0.44 +def5678\trefs/tags/v1.0.45-1 +ghi9012\trefs/tags/v1.0.45-10 +jkl3456\trefs/tags/v1.0.45-2 +mno7890\trefs/tags/v1.0.45" + +result2="$(echo "${MOCK_PRERELEASE}" | resolve_prerelease_version)" +check "picks highest prerelease (v1.0.45-10)" bash -c "[ '${result2}' = 'v1.0.45-10' ]" + +# Test: filters out non-version tags +MOCK_STRAY="abc1234\trefs/tags/latest +def5678\trefs/tags/v1.0.3 +ghi9012\trefs/tags/nightly +jkl3456\trefs/tags/v1.0.20" + +result3="$(echo "${MOCK_STRAY}" | resolve_prerelease_version)" +check "ignores non-version tags" bash -c "[ '${result3}' = 'v1.0.20' ]" + +# Report result +reportResults diff --git a/test/copilot-cli/scenarios.json b/test/copilot-cli/scenarios.json new file mode 100644 index 000000000..87fcc541d --- /dev/null +++ b/test/copilot-cli/scenarios.json @@ -0,0 +1,10 @@ +{ + "resolve_prerelease_version": { + "image": "ubuntu:noble", + "features": { + "copilot-cli": { + "version": "latest" + } + } + } +} From fb8ecf611d47cfad9b1ca983170b099d530e5eea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 11:40:40 +0000 Subject: [PATCH 4/4] fix(copilot-cli): make repo_url mandatory in resolve_prerelease_version Remove the stdin/cat fallback and use ${1:?} to error if no URL is provided. Update the test to mock git via PATH instead of piping stdin. --- src/copilot-cli/install.sh | 13 +--- .../copilot-cli/resolve_prerelease_version.sh | 73 +++++++++---------- 2 files changed, 39 insertions(+), 47 deletions(-) diff --git a/src/copilot-cli/install.sh b/src/copilot-cli/install.sh index 519142f28..d4c27ad1a 100755 --- a/src/copilot-cli/install.sh +++ b/src/copilot-cli/install.sh @@ -45,17 +45,12 @@ download_from_github() { rm -rf /tmp/copilotcli } -# Resolves the latest prerelease version tag from git ls-remote output. +# Resolves the latest prerelease version tag from a remote repository. # Filters to well-formed vX.Y.Z or vX.Y.Z-N tags and sorts by version. -# Reads ls-remote formatted lines from stdin if no arguments are provided, -# otherwise runs git ls-remote --tags against the given repository URL. resolve_prerelease_version() { - local repo_url="${1:-}" - if [ -n "${repo_url}" ]; then - git ls-remote --tags "${repo_url}" - else - cat - fi | awk '{print $2}' | sed 's|refs/tags/||' \ + local repo_url="${1:?resolve_prerelease_version requires a repository URL}" + git ls-remote --tags "${repo_url}" \ + | awk '{print $2}' | sed 's|refs/tags/||' \ | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$' \ | sort -V | tail -n1 } diff --git a/test/copilot-cli/resolve_prerelease_version.sh b/test/copilot-cli/resolve_prerelease_version.sh index 1ff182a2b..8369788c8 100644 --- a/test/copilot-cli/resolve_prerelease_version.sh +++ b/test/copilot-cli/resolve_prerelease_version.sh @@ -5,53 +5,50 @@ set -e # Optional: Import test library source dev-container-features-test-lib -# Source the install script's resolve_prerelease_version function -# We extract it by sourcing, but we only need the function definition -eval "$(sed -n '/^resolve_prerelease_version()/,/^}/p' /usr/local/share/copilot-cli-install.sh 2>/dev/null || true)" - -# If sourcing didn't work, define it inline (matches src/copilot-cli/install.sh) -if ! type resolve_prerelease_version &>/dev/null; then - resolve_prerelease_version() { - local repo_url="${1:-}" - if [ -n "${repo_url}" ]; then - git ls-remote --tags "${repo_url}" - else - cat - fi | awk '{print $2}' | sed 's|refs/tags/||' \ - | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$' \ - | sort -V | tail -n1 - } -fi - -# Mock git ls-remote output (alphabetically v1.0.9 > v1.0.45) -MOCK_LS_REMOTE="abc1234\trefs/tags/v1.0.1 -def5678\trefs/tags/v1.0.9 -ghi9012\trefs/tags/v1.0.10 -jkl3456\trefs/tags/v1.0.45 -mno7890\trefs/tags/v1.0.2" +# Define the function under test (matches src/copilot-cli/install.sh) +resolve_prerelease_version() { + local repo_url="${1:?resolve_prerelease_version requires a repository URL}" + git ls-remote --tags "${repo_url}" \ + | awk '{print $2}' | sed 's|refs/tags/||' \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$' \ + | sort -V | tail -n1 +} + +# Create a mock git script that returns controlled output +MOCK_DIR="$(mktemp -d)" +cat > "${MOCK_DIR}/git" << 'SCRIPT' +#!/bin/bash +# Return mock ls-remote output based on the repo URL argument +repo="${*: -1}" +case "${repo}" in + "mock://basic") + printf 'abc1234\trefs/tags/v1.0.1\ndef5678\trefs/tags/v1.0.9\nghi9012\trefs/tags/v1.0.10\njkl3456\trefs/tags/v1.0.45\nmno7890\trefs/tags/v1.0.2\n' + ;; + "mock://prerelease") + printf 'abc1234\trefs/tags/v1.0.44\ndef5678\trefs/tags/v1.0.45-1\nghi9012\trefs/tags/v1.0.45-10\njkl3456\trefs/tags/v1.0.45-2\nmno7890\trefs/tags/v1.0.45\n' + ;; + "mock://stray") + printf 'abc1234\trefs/tags/latest\ndef5678\trefs/tags/v1.0.3\nghi9012\trefs/tags/nightly\njkl3456\trefs/tags/v1.0.20\n' + ;; +esac +SCRIPT +chmod +x "${MOCK_DIR}/git" +export PATH="${MOCK_DIR}:${PATH}" # Test: version sort picks v1.0.45, not v1.0.9 -result="$(echo "${MOCK_LS_REMOTE}" | resolve_prerelease_version)" +result="$(resolve_prerelease_version "mock://basic")" check "picks highest version (v1.0.45)" bash -c "[ '${result}' = 'v1.0.45' ]" # Test: prerelease numeric suffixes sort correctly -MOCK_PRERELEASE="abc1234\trefs/tags/v1.0.44 -def5678\trefs/tags/v1.0.45-1 -ghi9012\trefs/tags/v1.0.45-10 -jkl3456\trefs/tags/v1.0.45-2 -mno7890\trefs/tags/v1.0.45" - -result2="$(echo "${MOCK_PRERELEASE}" | resolve_prerelease_version)" +result2="$(resolve_prerelease_version "mock://prerelease")" check "picks highest prerelease (v1.0.45-10)" bash -c "[ '${result2}' = 'v1.0.45-10' ]" # Test: filters out non-version tags -MOCK_STRAY="abc1234\trefs/tags/latest -def5678\trefs/tags/v1.0.3 -ghi9012\trefs/tags/nightly -jkl3456\trefs/tags/v1.0.20" - -result3="$(echo "${MOCK_STRAY}" | resolve_prerelease_version)" +result3="$(resolve_prerelease_version "mock://stray")" check "ignores non-version tags" bash -c "[ '${result3}' = 'v1.0.20' ]" +# Cleanup +rm -rf "${MOCK_DIR}" + # Report result reportResults