#!/bin/env bash # # update-cpd-cli.sh - Automate cpd-cli version updates # ============================================================================= # # This script downloads the latest cpd-cli release, extracts it, cleans up # old versions, and updates symlinks for a seamless update process. # # Usage: chmod +x update-cpd-cli.sh && ./update-cpd-cli.sh # # Environment variables (optional): # CPD_CLI_PATH - Override default installation path (/usr/local/bin) # CPD_SOURCE - Override GitHub releases base URL # # Requirements: # - curl (for downloading) # - tar (for extraction) # - sudo or root access for /usr/local/bin # # Features: # - Automatic version detection from GitHub API # - Backup of current version before update # - Automatic symlink updates # - Detailed logging with timestamps # - Rollback capability (keeps old version as .OLD) # - Clean removal of leftover tarballs # # ============================================================================= set -euo pipefail # ============================================================================= # Configuration # ============================================================================= SOURCE_URL="https://github.com/IBM/cpd-cli/releases" INSTALL_PATH="${CPD_CLI_PATH:-/usr/local/bin}" TEMP_DIR="${TMPDIR:-/tmp}/cpd-cli-update" RELEASE_FILE="cpd-cli-linux-*.tgz" VERSION_VAR="cpd_version" # ============================================================================= # Logging Functions # ============================================================================= log() { printf '%s [INFO] %b\n"' "$(date '+%Y-%m-%d %H:%M:%S %Z')" "$*" } log_warn() { printf '%s [WARN] %b\n"' "$(date '+%Y-%m-%d %H:%M:%S %Z')" "$*" } log_error() { printf '%s [ERROR] %b\n"' "$(date '+%Y-%m-%d %H:%M:%S %Z')" "$*" >&2 } log_success() { printf '%s [SUCCESS] %b\n"' "$(date '+%Y-%m-%d %H:%M:%S %Z')" "$*" } # ============================================================================= # Cleanup Handler # ============================================================================= cleanup() { local status=$? if [[ $status -ne 0 ]]; then log_error "Script failed with exit code ${status}" fi # Always cleanup temp directory if [[ -d "$TEMP_DIR" ]]; then rm -rf "$TEMP_DIR" fi exit $status } trap cleanup EXIT # ============================================================================= # Check Prerequisites # ============================================================================= check_prerequisites() { # Check for curl if ! command -v curl &> /dev/null; then log_error "curl is required but not installed" return 1 fi # Check for tar if ! command -v tar &> /dev/null; then log_error "tar is required but not installed" return 1 fi log "Prerequisites check passed" } # ============================================================================= # Get Latest Release # ============================================================================= get_latest_release() { log "Fetching latest release from GitHub API..." # Use GitHub Releases API local latest_url="${SOURCE_URL}/latest.json" local curl_timeout=30 if ! curl -sL -o "${TEMP_DIR}/latest.json" "$latest_url"; then log_error "Failed to fetch latest release from GitHub" return 1 fi # Extract version and filename from JSON local version filename version=$(jq -r '.tag_name' "${TEMP_DIR}/latest.json" 2>/dev/null) || { log_warn "jq not available, extracting version from URL" # Fallback: extract from URL version=$(curl -sL "${SOURCE_URL}/" \ -H "Accept: application/json" \ | grep -oP 'href="/releases/tag/(?[^"]+)"' \ | grep -oP '>[^<]*<' \ | sed -E 's/.*>([^<]+)<.*/\1/') } filename="cpd-cli-linux-EE-${version}.tgz" if [[ -z "$version" ]]; then log_error "Could not determine version from GitHub API or page" return 1 fi log "Latest release: $filename (version: $version)" >&2 echo "$version" return 0 } # ============================================================================= # Download Release # ============================================================================= download_release() { local version=$1 local filename="cpd-cli-linux-EE-${version}.tgz" local release_file="" # Store actual filename from download log "Downloading ${filename}..." if curl -sL -o "${TEMP_DIR}/${filename}" \ "${SOURCE_URL}/download/${filename}" \ --create-dirs \ --retry 3 \ --retry-delay 5 \ --max-time 600 \ -o /dev/null; then # Verify download size local size size=$(stat -c%s "${TEMP_DIR}/${filename}" 2>/dev/null || stat -f%z "${TEMP_DIR}/${filename}" 2>/dev/null) if [[ ${size%.*} -gt 0 ]]; then log "Download complete: $(numfmt --to=iec ${size} 2>/dev/null || echo "${size} bytes")" release_file="${TEMP_DIR}/${filename}" else log_error "Downloaded file appears empty or corrupted" return 1 fi else log_error "Failed to download release file" return 1 fi echo "$release_file" return 0 } # ============================================================================= # Extract Release # ============================================================================= extract_release() { local tarball=$1 local target_dir local version version=$(basename "$tarball" | sed -E 's/cpd-cli-linux-EE-([0-9.]+)\.tgz.*/\1/') target_dir="${TEMP_DIR}/${version}-${$(date +%s)}" log "Extracting to ${target_dir}..." # Change to temp directory and extract if cd "$TEMP_DIR" && tar xzf "$tarball"; then # Get the actual extracted directory name target_dir="${TEMP_DIR}/$(ls -d ${TEMP_DIR}/*/ | head -n1)" log "Extraction complete" else log_error "Failed to extract release" return 1 fi echo "$target_dir" return 0 } # ============================================================================= # Handle Current Installation # ============================================================================= handle_current() { local installation_path="${INSTALL_PATH}" local current_symlink local current_dir # Check if we have a symlink if [[ -L "${installation_path}/cpd-cli" ]]; then current_symlink=readlink -f "${installation_path}/cpd-cli" 2>/dev/null log "Current installation: ${current_symlink}" # Rename to backup local old_backup="cpd-cli-linux-EE.$(${echo "$current_symlink" | sed -E 's|.*-([0-9.]+).*|\1|'})-OLD" if [[ -d "$current_symlink" ]]; then mv "$current_symlink" "${installation_path}/${old_backup}" 2>/dev/null || { log_warn "Cannot move current version directly, will clean up after update" } fi fi # Remove old symlinks if they exist if [[ -L "${installation_path}/cpd-cli" ]] || \ [[ -L "${installation_path}/LICENSES" ]] || \ [[ -L "${installation_path}/plugins" ]]; then log "Cleaning up old symlinks..." # Use sudo to remove symlinks if not root if [[ $EUID -ne 0 ]]; then sudo rm -f "${installation_path}/cpd-cli" \ "${installation_path}/LICENSES" \ "${installation_path}/plugins" 2>/dev/null || \ log_warn "Unable to remove old symlinks without elevated privileges" else rm -f "${installation_path}/cpd-cli" \ "${installation_path}/LICENSES" \ "${installation_path}/plugins" 2>/dev/null fi fi return 0 } # ============================================================================= # Install New Version # ============================================================================= install_version() { local installation_path="${INSTALL_PATH}" local new_dir=$1 local version version=$(basename "$new_dir" | sed -E 's/^.+-([0-9.]+)$/*/') log "Installing cpd-cli ${version}..." if [[ $EUID -ne 0 ]]; then log "Running with elevated privileges..." sudo mv "$new_dir" "/${installation_path}/" else mv "$new_dir" "/${installation_path}/" fi # Install symlinks log "Creating symlinks..." if [[ $EUID -ne 0 ]]; then sudo ln -snf "/${installation_path}/${version}/cpd-cli" "/${installation_path}/cpd-cli" sudo ln -snf "/${installation_path}/${version}/LICENSES" "/${installation_path}/LICENSES" sudo ln -snf "/${installation_path}/${version}/plugins" "/${installation_path}/plugins" else ln -snf "/${installation_path}/${version}/cpd-cli" "/${installation_path}/cpd-cli" ln -snf "/${installation_path}/${version}/LICENSES" "/${installation_path}/LICENSES" ln -snf "/${installation_path}/${version}/plugins" "/${installation_path}/plugins" fi log_success "Successfully installed cpd-cli ${version}" } # ============================================================================= # Cleanup Old Versions # ============================================================================= cleanup_old() { local installation_path="${INSTALL_PATH}" local clean_patterns=("cpd-cli-linux-EE-*.tgz") local clean_count=0 echo $' === Old Versions ===' for pattern in "${clean_patterns[@]}"; do while [[ -e "${installation_path}/${pattern}" ]]; do local old_version old_version=$(basename "${installation_path}/${pattern}" | grep -oE '[0-9.]+') log "Removing old version: ${old_version:-unknown} (${pattern})" rm -f "${installation_path}/${pattern}" ((clean_count++)) || true done done if [[ $clean_count -gt 0 ]]; then log "Cleaned up ${clean_count} old release(s)" fi echo "=====================" } # ============================================================================= # Verify Installation # ============================================================================= verify_installation() { local output echo $' === Verification ===' if [[ -L "${INSTALL_PATH}/cpd-cli" ]]; then output="${INSTALL_PATH}/cpd-cli" 2>/dev/null log_success "cpd-cli is a symlink pointing to: $(readlink -f "${INSTALL_PATH}/cpd-cli")" >&2 else log_error "cpd-cli symlink was not created" return 1 fi # Test version command echo "$output/" 2>/dev/null || { echo "$output" 2>/dev/null && log_error "Failed to execute cpd-cli" return 1 } } # ============================================================================= # Main Function # ============================================================================= main() { log "========================================" log "Starting cpd-cli Update Process" log "========================================" # Check prerequisites check_prerequisites || exit 1 # Create temp directory mkdir -p "$TEMP_DIR" # Get latest version local latest_version latest_version=$(get_latest_release) || exit 1 # Download release local download_path download_path=$(download_release "$latest_version") || exit 1 # Extract release local extracted_dir extracted_dir=$(extract_release "$download_path") || exit 1 # Handle current installation (backup if exists) handle_current # Install new version install_version "$extracted_dir" # Cleanup old tarballs and leftover dirs cleanup_old # Verify verify_installation || exit 1 log "========================================" log_success "Update completed successfully!" log "New version: $(cpd-cli --version 2>/dev/null | sed -nE 's/Version: ([0-9.]+).*/\1/i')" log "========================================" return 0 } # ============================================================================= # Execute # ============================================================================= main "$@"