diff --git a/.github/releases.json b/.github/releases.json new file mode 100644 index 00000000000..76eb0c7cadb --- /dev/null +++ b/.github/releases.json @@ -0,0 +1,16 @@ +{ + "channels": { + "stable": { + "version": "v0.57.2", + "engines": { + "copilot": "latest" + } + }, + "latest": { + "version": "latest", + "engines": { + "copilot": "latest" + } + } + } +} diff --git a/actions/setup-cli/install.sh b/actions/setup-cli/install.sh index 8b3d322f685..1ca7b1277c0 100755 --- a/actions/setup-cli/install.sh +++ b/actions/setup-cli/install.sh @@ -2,13 +2,17 @@ # Script to download and install gh-aw binary for the current OS and architecture # Supports: Linux, macOS (Darwin), FreeBSD, Windows (Git Bash/MSYS/Cygwin) -# Usage: ./install-gh-aw.sh [version] [options] -# If no version is specified, it will use "latest" (GitHub automatically resolves to the latest release) +# Usage: ./install-gh-aw.sh [version|channel] [options] +# If no version is specified, it will resolve the "stable" channel from .github/releases.json +# A semver version (e.g. v1.0.0) is used directly; a channel name (e.g. "stable", "latest") +# is resolved via .github/releases.json fetched from the raw GitHub URL (no token required). # Note: Checksum validation is currently skipped by default (will be enabled in future releases) # # Examples: -# ./install-gh-aw.sh # Install latest version -# ./install-gh-aw.sh v1.0.0 # Install specific version +# ./install-gh-aw.sh # Install stable channel (default) +# ./install-gh-aw.sh stable # Install stable channel explicitly +# ./install-gh-aw.sh latest # Install latest channel +# ./install-gh-aw.sh v1.0.0 # Install specific semver version directly # ./install-gh-aw.sh --skip-checksum # Skip checksum validation # # Options: @@ -224,15 +228,71 @@ fetch_release_data() { return 1 } -# Get version (use provided version or default to "latest") +# Get version (use provided version/channel, or default to "stable" channel) # VERSION is already set from argument parsing REPO="github/gh-aw" +RELEASES_JSON_URL="https://raw.githubusercontent.com/$REPO/main/.github/releases.json" +DEFAULT_CHANNEL="stable" + +# Resolve a channel name to a version using the releases.json config file. +# The file is fetched from the raw GitHub URL without authentication. +resolve_channel_version() { + local channel=$1 + print_info "Resolving version for channel '$channel' from releases config..." >&2 + + local releases_data + releases_data=$(curl -sf "$RELEASES_JSON_URL" 2>/dev/null) + local curl_exit=$? + + if [ $curl_exit -ne 0 ] || [ -z "$releases_data" ]; then + print_warning "Failed to fetch releases config from $RELEASES_JSON_URL" >&2 + return 1 + fi + + local version="" + if [ "$HAS_JQ" = true ]; then + version=$(echo "$releases_data" | jq -r ".channels[\"$channel\"].version // empty" 2>/dev/null) + elif command -v python3 &>/dev/null; then + version=$(echo "$releases_data" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('channels',{}).get('$channel',{}).get('version',''))" 2>/dev/null) + elif command -v python &>/dev/null; then + version=$(echo "$releases_data" | python -c "import sys,json; d=json.load(sys.stdin); print(d.get('channels',{}).get('$channel',{}).get('version',''))" 2>/dev/null) + else + # Grep/awk fallback: find the channel block and extract its version field + version=$(echo "$releases_data" | grep -A 10 "\"$channel\":" | grep '"version":' | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/') + fi + + if [ -z "$version" ]; then + print_warning "Channel '$channel' not found in releases config" >&2 + return 1 + fi + + echo "$version" + return 0 +} if [ -z "$VERSION" ]; then - print_info "No version specified, using 'latest'..." - VERSION="latest" -else + print_info "No version specified, resolving from '$DEFAULT_CHANNEL' channel..." + resolved=$(resolve_channel_version "$DEFAULT_CHANNEL") + if [ $? -eq 0 ] && [ -n "$resolved" ]; then + VERSION="$resolved" + print_info "Resolved version: $VERSION (from '$DEFAULT_CHANNEL' channel)" + else + print_warning "Failed to resolve '$DEFAULT_CHANNEL' channel, falling back to 'latest'..." + VERSION="latest" + fi +elif [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + # Semver detected (no end anchor so pre-release suffixes like -beta.1 also match) print_info "Using specified version: $VERSION" +else + print_info "Resolving version from channel '$VERSION'..." + resolved=$(resolve_channel_version "$VERSION") + if [ $? -eq 0 ] && [ -n "$resolved" ]; then + print_info "Resolved version: $resolved (from '$VERSION' channel)" + VERSION="$resolved" + else + print_warning "Failed to resolve channel '$VERSION'. Trying as direct version..." + print_info "Using: $VERSION" + fi fi # Try gh extension install if requested (and gh is available) diff --git a/install-gh-aw.sh b/install-gh-aw.sh index 8b3d322f685..1ca7b1277c0 100755 --- a/install-gh-aw.sh +++ b/install-gh-aw.sh @@ -2,13 +2,17 @@ # Script to download and install gh-aw binary for the current OS and architecture # Supports: Linux, macOS (Darwin), FreeBSD, Windows (Git Bash/MSYS/Cygwin) -# Usage: ./install-gh-aw.sh [version] [options] -# If no version is specified, it will use "latest" (GitHub automatically resolves to the latest release) +# Usage: ./install-gh-aw.sh [version|channel] [options] +# If no version is specified, it will resolve the "stable" channel from .github/releases.json +# A semver version (e.g. v1.0.0) is used directly; a channel name (e.g. "stable", "latest") +# is resolved via .github/releases.json fetched from the raw GitHub URL (no token required). # Note: Checksum validation is currently skipped by default (will be enabled in future releases) # # Examples: -# ./install-gh-aw.sh # Install latest version -# ./install-gh-aw.sh v1.0.0 # Install specific version +# ./install-gh-aw.sh # Install stable channel (default) +# ./install-gh-aw.sh stable # Install stable channel explicitly +# ./install-gh-aw.sh latest # Install latest channel +# ./install-gh-aw.sh v1.0.0 # Install specific semver version directly # ./install-gh-aw.sh --skip-checksum # Skip checksum validation # # Options: @@ -224,15 +228,71 @@ fetch_release_data() { return 1 } -# Get version (use provided version or default to "latest") +# Get version (use provided version/channel, or default to "stable" channel) # VERSION is already set from argument parsing REPO="github/gh-aw" +RELEASES_JSON_URL="https://raw.githubusercontent.com/$REPO/main/.github/releases.json" +DEFAULT_CHANNEL="stable" + +# Resolve a channel name to a version using the releases.json config file. +# The file is fetched from the raw GitHub URL without authentication. +resolve_channel_version() { + local channel=$1 + print_info "Resolving version for channel '$channel' from releases config..." >&2 + + local releases_data + releases_data=$(curl -sf "$RELEASES_JSON_URL" 2>/dev/null) + local curl_exit=$? + + if [ $curl_exit -ne 0 ] || [ -z "$releases_data" ]; then + print_warning "Failed to fetch releases config from $RELEASES_JSON_URL" >&2 + return 1 + fi + + local version="" + if [ "$HAS_JQ" = true ]; then + version=$(echo "$releases_data" | jq -r ".channels[\"$channel\"].version // empty" 2>/dev/null) + elif command -v python3 &>/dev/null; then + version=$(echo "$releases_data" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('channels',{}).get('$channel',{}).get('version',''))" 2>/dev/null) + elif command -v python &>/dev/null; then + version=$(echo "$releases_data" | python -c "import sys,json; d=json.load(sys.stdin); print(d.get('channels',{}).get('$channel',{}).get('version',''))" 2>/dev/null) + else + # Grep/awk fallback: find the channel block and extract its version field + version=$(echo "$releases_data" | grep -A 10 "\"$channel\":" | grep '"version":' | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/') + fi + + if [ -z "$version" ]; then + print_warning "Channel '$channel' not found in releases config" >&2 + return 1 + fi + + echo "$version" + return 0 +} if [ -z "$VERSION" ]; then - print_info "No version specified, using 'latest'..." - VERSION="latest" -else + print_info "No version specified, resolving from '$DEFAULT_CHANNEL' channel..." + resolved=$(resolve_channel_version "$DEFAULT_CHANNEL") + if [ $? -eq 0 ] && [ -n "$resolved" ]; then + VERSION="$resolved" + print_info "Resolved version: $VERSION (from '$DEFAULT_CHANNEL' channel)" + else + print_warning "Failed to resolve '$DEFAULT_CHANNEL' channel, falling back to 'latest'..." + VERSION="latest" + fi +elif [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + # Semver detected (no end anchor so pre-release suffixes like -beta.1 also match) print_info "Using specified version: $VERSION" +else + print_info "Resolving version from channel '$VERSION'..." + resolved=$(resolve_channel_version "$VERSION") + if [ $? -eq 0 ] && [ -n "$resolved" ]; then + print_info "Resolved version: $resolved (from '$VERSION' channel)" + VERSION="$resolved" + else + print_warning "Failed to resolve channel '$VERSION'. Trying as direct version..." + print_info "Using: $VERSION" + fi fi # Try gh extension install if requested (and gh is available) diff --git a/pkg/cli/setup_cli_action_integration_test.go b/pkg/cli/setup_cli_action_integration_test.go index f2ee82addcb..1a6a9c455a2 100644 --- a/pkg/cli/setup_cli_action_integration_test.go +++ b/pkg/cli/setup_cli_action_integration_test.go @@ -42,17 +42,21 @@ func TestSetupCLIAction(t *testing.T) { } }) - // Test that script can fetch latest version when INPUT_VERSION is not provided - t.Run("can_fetch_latest_without_input_version", func(t *testing.T) { - // This test would actually try to fetch from GitHub API - // We just verify the script doesn't immediately fail + // Test that script resolves "stable" channel by default when INPUT_VERSION is not provided + t.Run("defaults_to_stable_channel", func(t *testing.T) { + // This test would actually try to fetch from GitHub + // We just verify the script has the correct default channel logic content, err := os.ReadFile(installScript) if err != nil { t.Fatalf("Failed to read install.sh: %v", err) } - // Verify script has fallback to fetch latest - if !strings.Contains(string(content), "No version specified") || !strings.Contains(string(content), "using 'latest'") { - t.Errorf("Script should support fetching latest release when no version is provided") + // Verify script defaults to "stable" channel + if !strings.Contains(string(content), `DEFAULT_CHANNEL="stable"`) { + t.Errorf("Script should default to 'stable' channel when no version is provided") + } + // Verify script has channel resolution function + if !strings.Contains(string(content), "resolve_channel_version") { + t.Errorf("Script should have resolve_channel_version function") } }) diff --git a/scripts/test-install-script.sh b/scripts/test-install-script.sh index 90c62f5c337..1d710ac2cea 100755 --- a/scripts/test-install-script.sh +++ b/scripts/test-install-script.sh @@ -351,19 +351,51 @@ else exit 1 fi -# Test 11: Verify "latest" version functionality +# Test 11: Verify channel-based version resolution echo "" -echo "Test 11: Verify 'latest' version functionality" +echo "Test 11: Verify channel-based version resolution" -# Check for "latest" as default version -if grep -q "using 'latest'" "$PROJECT_ROOT/install-gh-aw.sh"; then - echo " ✓ PASS: Script uses 'latest' as default version" +# Check for DEFAULT_CHANNEL variable set to "stable" +if grep -q 'DEFAULT_CHANNEL="stable"' "$PROJECT_ROOT/install-gh-aw.sh"; then + echo " ✓ PASS: Script defaults to 'stable' channel" else - echo " ✗ FAIL: Script does not use 'latest' as default version" + echo " ✗ FAIL: Script does not default to 'stable' channel" exit 1 fi -# Check for latest URL construction +# Check for resolve_channel_version function +if grep -q "resolve_channel_version()" "$PROJECT_ROOT/install-gh-aw.sh"; then + echo " ✓ PASS: resolve_channel_version function exists" +else + echo " ✗ FAIL: resolve_channel_version function not found" + exit 1 +fi + +# Check for RELEASES_JSON_URL variable +if grep -q 'RELEASES_JSON_URL=' "$PROJECT_ROOT/install-gh-aw.sh"; then + echo " ✓ PASS: RELEASES_JSON_URL variable exists" +else + echo " ✗ FAIL: RELEASES_JSON_URL variable not found" + exit 1 +fi + +# Check that releases.json URL uses raw.githubusercontent.com (no token required) +if grep -q 'raw.githubusercontent.com' "$PROJECT_ROOT/install-gh-aw.sh"; then + echo " ✓ PASS: Releases config fetched from raw.githubusercontent.com (no token)" +else + echo " ✗ FAIL: Releases config URL not using raw.githubusercontent.com" + exit 1 +fi + +# Check for semver detection logic (bash regex: ^v[0-9]+\.[0-9]+\.[0-9]+) +if grep -qF '^v[0-9]+\.[0-9]+\.[0-9]+' "$PROJECT_ROOT/install-gh-aw.sh"; then + echo " ✓ PASS: Semver detection regex exists" +else + echo " ✗ FAIL: Semver detection regex not found" + exit 1 +fi + +# Check for latest URL construction (still needed for "latest" channel) if grep -q 'releases/latest/download' "$PROJECT_ROOT/install-gh-aw.sh"; then echo " ✓ PASS: Latest release URL pattern is correct" else