diff --git a/Makefile b/Makefile index 28c62ca..1298eec 100644 --- a/Makefile +++ b/Makefile @@ -149,25 +149,13 @@ tidy: clean: rm -rf $(BUILD_DIR) $(DIST_DIR) -.PHONY: help -help: - @echo "Targets:" - @echo " build build for the current platform into ./bin/" - @echo " build-all cross-compile darwin/linux/windows × amd64/arm64 into ./dist/" - @echo " build-one helper for build-all (GOOS=... GOARCH=...)" - @echo " goreleaser-build local snapshot build via GoReleaser (into ./dist/)" - @echo " goreleaser-release full release via GoReleaser (requires a git tag)" - @echo " release build-all + sha256 checksums (legacy)" - @echo " upload build-all + publish to S3 (BUCKET=... VERSION=...)" - @echo " upload-latest only update the 'latest' pointer in the bucket" - @echo " install go install into \$$GOBIN" - @echo " lint run golangci-lint" - @echo " test, tidy standard Go targets" - @echo " clean remove ./bin and ./dist" - openapi-generator: openapi-generator-cli generate -i openapi-public.yaml -g go -o ./internal/workflow_client -c .openapi-generator.yaml .PHONY: lint lint: golangci-lint run --fix + +.PHONY: gen-install +gen-install: + binst gen -o scripts/install.sh --config binstaller.yml diff --git a/README.md b/README.md index e17bc85..e4a9259 100644 --- a/README.md +++ b/README.md @@ -7,23 +7,6 @@ developer tools products. It currently covers two products: - **Workflow Studio** (`eds wf`) — wires a repository + branch to a deploy pipeline and publishes it. -Repo and Workflow Studio are independent products, but both are authenticated -with the same API key (`EDS_API_KEY`) via `X-API-KEY`. `eds` is just the -platform CLI they're both driven through. Every command is designed to be safely -driven by automation and AI agents: JSON output by default, environment variables -for secrets. - -- Single static binary (Go, no runtime dependencies). -- Reads the API key from `EDS_API_KEY` or from `~/.config/eds/config.json`. -- Outputs JSON by default. Use `--yaml` for YAML output. `--quiet` suppresses - all output (useful for fire-and-forget calls from agents). -- Repo uses the local `git` CLI for clone. Both products authenticate with - `X-API-KEY`. `eds repo clone` embeds the API key as HTTP Basic Auth - credentials directly into the smart-HTTP URL it passes to `git clone`, so - clone/push work standalone — no git credential helper, OS keychain, or - `~/.netrc` needs to be pre-configured. This matters for CI and AI agent - sandboxes, which typically have none of those. - ## Installation ### From a GitHub Release (recommended) @@ -31,8 +14,7 @@ for secrets. **macOS / Linux:** ```bash -curl -fsSL https://raw.githubusercontent.com/cloud-ru/evolution-devservices-cli/main/scripts/install.sh | bash -export PATH="$HOME/.local/bin:$PATH" +curl -fsSL https://raw.githubusercontent.com/cloud-ru/evolution-devservices-cli/main/scripts/install.sh | sudo bash -s -- -b /usr/local/bin/ eds version ``` @@ -43,36 +25,13 @@ iwr -useb https://raw.githubusercontent.com/cloud-ru/evolution-devservices-cli/m eds version ``` -The installer detects the platform, downloads the matching static binary -from the latest [GitHub Release](https://github.com/cloud-ru/evolution-devservices-cli/releases), -places it in `~/.local/bin/eds`, and verifies the install. - -You can also grab a binary directly from the -[releases page](https://github.com/cloud-ru/evolution-devservices-cli/releases/latest): +### Using Go ```bash -# pick eds-- for your platform, e.g. eds-darwin-arm64, eds-linux-amd64, eds-windows-amd64.exe -curl -fsSL -o eds \ - https://github.com/cloud-ru/evolution-devservices-cli/releases/latest/download/eds-darwin-arm64 -chmod +x eds && sudo mv eds /usr/local/bin/eds -``` - -### From source - -```bash -git clone git@github.com:cloud-ru/evolution-devservices-cli.git -cd evolution-devservices-cli go install github.com/cloud-ru/evolution-devservices-cli/cmd/eds@latest -# or -make build # ./bin/eds -make build-all # cross-compile darwin/linux/windows × amd64/arm64 into ./dist/ -# or, with GoReleaser (https://goreleaser.com) -make goreleaser-build # snapshot build into ./dist/ +eds version ``` -> Target platforms: **Linux + macOS + Windows** (developers locally + CI). -> Override the matrix if you ever need to: `make build-all OSES="linux darwin" ARCHS="amd64 arm64"`. - ## Configuration The CLI looks for configuration in this order (later wins): @@ -90,32 +49,6 @@ for Repo, `EDS_WF_*`/`--wf-*` for Workflow Studio — except for `--api-key`/ `EDS_API_KEY` and `--project`/`EDS_PROJECT_ID`, which are shared platform-level settings. -Example config file (`~/.config/eds/config.json`): - -```json -{ - "api_url": "https://devtools.api.cloud.ru/repo/api/v1", - "project_id": "3232b2d0-1063-41e6-b2fa-13df767f4a0a", - "api_key": "...", - "git_host": "https://repo.cloud.ru/", - "workflow_api_url": "https://devtools.api.cloud.ru/workflow/v1" -} -``` - -### Production vs dev (Repo product) - -- **Production** — `https://devtools.api.cloud.ru/repo/api/v1`, git host - `https://repo.cloud.ru/`. -- **Dev** — `https://devtools.dev.api.internal.cloud.ru/repo/api/v1`. - -Override with `--repo-api-url` or `EDS_REPO_API_URL`. - -### Workflow Studio auth - -Workflow Studio uses the same API key as Repo (`--api-key` / `EDS_API_KEY`), -sent as `X-API-KEY` on every request. There is no separate credential pair or -token exchange — the same key works for both `eds repo *` and `eds wf *`. - ## Commands ```text @@ -293,74 +226,6 @@ git tag v0.2.0 && git push origin v0.2.0 make goreleaser-release ``` -GoReleaser requires the `goreleaser` CLI to be installed: - -```bash -# macOS / Linux -brew install goreleaser -# or via Go -go install github.com/goreleaser/goreleaser/v2@latest -``` - -`scripts/install.sh` downloads from the latest GitHub Release by default. -The `Makefile` also has `make upload`/`make upload-latest` targets for -optionally mirroring builds to an S3-compatible bucket (e.g. for internal -environments without GitHub access) — pass `EDS_CLI_BASE_URL` to -`install.sh` to install from a mirror instead of GitHub: - -```bash -export AWS_ENDPOINT_URL=https://storage.cloud.ru -export AWS_ACCESS_KEY_ID=... -export AWS_SECRET_ACCESS_KEY=... -make upload BUCKET=my-bucket PREFIX=evolution-devservices-cli VERSION=v0.2.0 - -curl -fsSL https://storage.cloud.ru/my-bucket/evolution-devservices-cli/install.sh | \ - EDS_CLI_BASE_URL=https://storage.cloud.ru/my-bucket/evolution-devservices-cli bash - -# Or on Windows: -$env:EDS_CLI_BASE_URL = "https://storage.cloud.ru/my-bucket/evolution-devservices-cli" -iwr -useb https://storage.cloud.ru/my-bucket/evolution-devservices-cli/install.ps1 | iex -``` - -## Development - -```bash -make lint -make test # go test ./... -make build # current platform into ./bin/ -make build-all # full matrix into ./dist/ -make goreleaser-build # snapshot build via GoReleaser into ./dist/ -make clean # remove ./bin and ./dist -make help # list targets -``` - -Project layout: - -``` -cmd/ - eds/main.go # entry point + ldflags-driven version - root.go # cobra root command (`eds`) + global flags - helpers.go # config resolution, runtime context - login.go # `eds login` - config.go # `eds config` - version.go # `eds version` - repo.go # `eds repo list|create|show|delete|clone` - wf.go # `eds wf` parent command (groups app/run/job) - app.go # `eds wf app create|list|show|update|delete|deploy|deployments` - run.go # `eds wf run show|stop` - job.go # `eds wf job logs` -internal/ - config/ # disk config + env overrides - output/ # JSON / YAML formatting - repoapi/ # thin HTTP client for the Repo product API - workflow_client/ # generated OpenAPI client for Workflow Studio -scripts/ - install.sh # one-liner installer (GitHub Releases by default) -skills/ - eds/SKILL.md # Agent Skills description -Makefile # build, build-all, release, upload, … -``` - ## Environment variables | Variable | Description | diff --git a/binstaller.yml b/binstaller.yml new file mode 100644 index 0000000..51a373e --- /dev/null +++ b/binstaller.yml @@ -0,0 +1,9 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/binary-install/binstaller/main/schema/InstallSpec.json +schema: v1 +repo: cloud-ru/evolution-devservices-cli +name: eds +asset: + template: eds-${OS}-${ARCH}${EXT} +checksums: + algorithm: sha256 + template: checksums.txt diff --git a/scripts/install.sh b/scripts/install.sh index 2164c2a..9d21a93 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,166 +1,596 @@ -#!/usr/bin/env bash -# Self-installer for the Evolution DevServices CLI (eds). +#!/bin/sh +# Code generated by binstaller. DO NOT EDIT. # -# For Windows, use install.ps1 instead: -# iwr -useb https://raw.githubusercontent.com/cloud-ru/evolution-devservices-cli/main/scripts/install.ps1 | iex -# -# Usage: -# curl -fsSL https://raw.githubusercontent.com/cloud-ru/evolution-devservices-cli/main/scripts/install.sh | bash -# EDS_CLI_VERSION=v0.2.0 bash install.sh -# ./scripts/install.sh --version v0.2.0 --repo cloud-ru/evolution-devservices-cli -# -# The script: -# 1. Detects OS / architecture. -# 2. Downloads the matching static binary from a GitHub Release (default) -# or a custom mirror if EDS_CLI_BASE_URL/--base-url is set. -# 3. Installs it into $INSTALL_DIR (default ~/.local/bin). -# 4. Verifies the install with `eds version`. -# -# Environment variables (overridable via flags): -# EDS_CLI_VERSION - tag/version to download (default: latest) -# EDS_CLI_REPO - GitHub "org/repo" to fetch releases from -# (default: cloud-ru/evolution-devservices-cli) -# EDS_CLI_BASE_URL - base URL of a custom artifact mirror; when set, this -# is used instead of GitHub Releases (e.g. an internal -# S3-compatible bucket published via `make upload`) -# EDS_CLI_DIR - install directory (default ~/.local/bin) -# EDS_CLI_BIN - binary name (default: eds) -# EDS_CLI_QUIET - if set to 1, suppress progress output - -set -euo pipefail - -VERSION="${EDS_CLI_VERSION:-latest}" -REPO="${EDS_CLI_REPO:-cloud-ru/evolution-devservices-cli}" -BASE_URL="${EDS_CLI_BASE_URL:-}" -INSTALL_DIR="${EDS_CLI_DIR:-$HOME/.local/bin}" -BIN_NAME="${EDS_CLI_BIN:-eds}" -QUIET="${EDS_CLI_QUIET:-}" - +set -e usage() { + this=$1 cat <] [--repo ] [--base-url ] [--dir ] [--bin ] +$this: download ${NAME} from ${REPO} -Defaults can also be passed via env vars: EDS_CLI_VERSION, EDS_CLI_REPO, -EDS_CLI_BASE_URL, EDS_CLI_DIR, EDS_CLI_BIN. +Usage: $this [-b bindir] [-d] [-q] [-n] [tag] + -b sets bindir or installation directory, Defaults to ${BINSTALLER_BIN:-${HOME}/.local/bin} + -d turns on debug logging + -q turns on quiet mode (errors only) + -n turns on dry run mode + [tag] is a tag from + https://github.com/cloud-ru/evolution-devservices-cli/releases + If tag is missing, then latest will be used. -By default, binaries are downloaded from GitHub Releases at -https://github.com/\${EDS_CLI_REPO}/releases. Pass --base-url (or set -EDS_CLI_BASE_URL) to fetch from a custom mirror instead. +Environment variables: + BINSTALLER_NO_PROGRESS=1 Disable progress indicators + BINSTALLER_OS=... Override OS detection + BINSTALLER_ARCH=... Override architecture detection -Examples: - bash install.sh - EDS_CLI_VERSION=v0.2.0 bash install.sh - EDS_CLI_BASE_URL=https://storage.cloud.ru/evolution-devservices-cli bash install.sh + Generated by binstaller + https://github.com/binary-install/binstaller EOF + exit 2 } -while [[ $# -gt 0 ]]; do - case "$1" in - --version) VERSION="$2"; shift 2 ;; - --repo) REPO="$2"; shift 2 ;; - --base-url) BASE_URL="$2"; shift 2 ;; - --dir) INSTALL_DIR="$2"; shift 2 ;; - --bin) BIN_NAME="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown flag: $1" >&2; usage; exit 2 ;; +cat /dev/null </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + msys*) os="windows" ;; + mingw*) os="windows" ;; + cygwin*) os="windows" ;; esac -done + if [ "$os" = "sunos" ]; then + if [ "$(uname -o)" = "illumos" ]; then + os="illumos" + else + os="solaris" + fi + fi + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + i86pc) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo "${arch}" +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + midnightbsd) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + illumos) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +cat /dev/null <&2 + +hash_sha256() { + TARGET=${1:-/dev/stdin} + if is_command gsha256sum; then + hash=$(gsha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command sha256sum; then + hash=$(sha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command shasum; then + hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 2 + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi } -# -- detect platform --------------------------------------------------------- +hash_compute() { + hash_sha256 "$1" +} -OS_RAW="$(uname -s)" -ARCH_RAW="$(uname -m)" -case "$OS_RAW" in - Linux) OS=linux ;; - Darwin) OS=darwin ;; - MINGW*|MSYS*|CYGWIN*) OS=windows ;; - *) - echo "Unsupported OS: $OS_RAW" >&2 - exit 1 - ;; -esac +# shellcheck shell=sh +# Terminal progress reporting functions +progress_init() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + # OSC 9;4 sequences are safely ignored by unsupporting terminals + # Only need special handling for tmux passthrough + if [ -n "$TMUX" ]; then + # Tmux passthrough: DCS tmux; ST + # ESC characters in the wrapped sequence must be doubled + # Format: ESC P tmux; ESC ESC ] 9;4; ... ESC ESC \ ESC \ + PROGRESS_START=$(printf '\033Ptmux;\033\033]9;4;') + # shellcheck disable=SC1003 + PROGRESS_END=$(printf '\033\033\\\033\\') + else + # Direct OSC 9;4 - terminals that don't support it will safely ignore + PROGRESS_START=$(printf '\033]9;4;') + PROGRESS_END=$(printf '\007') + fi +} + +# Start pulsing progress animation +progress_pulse_start() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + + # Send OSC 9;4 with state 3 (indeterminate/pulsing) once + # The terminal will handle the continuous animation + printf "%s3%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} + +# Clear progress indicator +progress_clear() { + # Only show progress on interactive terminals and when not disabled + if [ ! -t 2 ] || [ "${BINSTALLER_NO_PROGRESS}" = "1" ]; then + return 0 + fi + printf "%s0;%s" "$PROGRESS_START" "$PROGRESS_END" >&2 +} -case "$ARCH_RAW" in - x86_64|amd64) ARCH=amd64 ;; - aarch64|arm64) ARCH=arm64 ;; +untar() { + tarball=$1 + strip_components=${2:-0} # default 0 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.xz) tar --no-same-owner -xJf "${tarball}" --strip-components "${strip_components}" ;; + *.tar.bz2) tar --no-same-owner -xjf "${tarball}" --strip-components "${strip_components}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" --strip-components "${strip_components}" ;; + *.gz) gunzip "${tarball}" ;; + *.zip) + # unzip doesn't have a standard --strip-components + # Workaround: extract to a subdir and move contents up if stripping + if [ "$strip_components" -gt 0 ]; then + extract_dir=$(basename "${tarball%.zip}")_extracted + unzip -q "${tarball}" -d "${extract_dir}" + # Move contents of the *first* directory found inside extract_dir up + # This assumes wrap_in_directory=true convention + first_subdir=$(find "${extract_dir}" -mindepth 1 -maxdepth 1 -type d -print -quit) + if [ -n "$first_subdir" ]; then + # Move all contents (* includes hidden files) + mv "${first_subdir}"/* . + # Optionally remove the now-empty subdir and the extract_dir + rmdir "${first_subdir}" + rmdir "${extract_dir}" + else + log_warn "Could not find subdirectory in zip to strip components from ${extract_dir}" + # Files are extracted in current dir anyway, proceed + fi + else + unzip -q "${tarball}" + fi + ;; *) - echo "Unsupported architecture: $ARCH_RAW" >&2 - exit 1 + log_err "untar unknown archive format for ${tarball}" + return 1 ;; -esac + esac +} -EXT="" -[[ "$OS" == "windows" ]] && EXT=".exe" -ASSET="eds-${OS}-${ARCH}${EXT}" -# -- resolve download URL ----------------------------------------------------- -# -# Two modes: -# - GitHub Releases (default): resolves "latest" via the GitHub API and -# downloads from the release's asset URL. -# - Custom mirror (--base-url/EDS_CLI_BASE_URL): same bucket layout as -# `make upload` publishes (//, /latest). - -if [[ -n "$BASE_URL" ]]; then - BASE_URL="${BASE_URL%/}" - if [[ "$VERSION" == "latest" ]]; then - VERSION="$(curl -fsSL "${BASE_URL}/latest" 2>/dev/null || true)" - if [[ -z "$VERSION" ]]; then - echo "Could not resolve 'latest' version from ${BASE_URL}/latest" >&2 +hash_verify() { + TARGET_PATH=$1 + SUMFILE=$2 + if [ -z "${SUMFILE}" ]; then + log_err "hash_verify checksum file not specified in arg2" + return 1 + fi + got=$(hash_compute "$TARGET_PATH") + if [ -z "${got}" ]; then + log_err "failed to calculate hash: ${TARGET_PATH}" + return 1 + fi + + BASENAME=${TARGET_PATH##*/} + + # Check for line matches in checksum file + # Format: " " or " *" + # Filename may include path prefix (e.g., "deployment/m2/file.tar.gz") + while IFS= read -r line || [ -n "$line" ]; do + # Normalize tabs to spaces + line=$(echo "$line" | tr '\t' ' ') + + # Remove trailing spaces for hash-only line check + line_trimmed=$(echo "$line" | sed 's/[[:space:]]*$//') + + # Check for hash-only line (no filename) - early return + if [ "$line_trimmed" = "$got" ]; then + return 0 + fi + + # Extract hash and filename parts + # First field is the hash, rest is filename (which may contain spaces) + line_hash=$(echo "$line" | cut -d' ' -f1) + + # Skip if hash doesn't match + if [ "$line_hash" != "$got" ]; then + continue + fi + + # Hash matches, now check filename + # Remove the hash part from the beginning of the line + line_rest="${line#"$got"}" + # Remove leading spaces + while [ "${line_rest#[ ]}" != "$line_rest" ]; do + line_rest="${line_rest#[ ]}" + done + + # Remove leading asterisk if present (binary mode indicator) + if [ "${line_rest#\*}" != "$line_rest" ]; then + line_rest="${line_rest#\*}" + fi + + # Extract just the filename without any path + line_filename="${line_rest##*/}" + + # Check if the filename matches + if [ "$line_filename" = "$BASENAME" ]; then + return 0 + fi + done < "$SUMFILE" + + log_err "hash_verify checksum for '$TARGET_PATH' did not verify" + log_err " Expected hash: ${got}" + log_err " Checksum file content:" + cat "$SUMFILE" >&2 + return 1 +} + +# GitHub HTTP download functions with GITHUB_TOKEN support +github_http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -o "$local_file" "$source_url" + else + curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" -H "$header" -o "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + curl -fsSL -o "$local_file" "$source_url" + else + curl -fsSL -H "$header" -o "$local_file" "$source_url" + fi + fi +} +github_http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -n "$GITHUB_TOKEN" ]; then + log_debug "Using GITHUB_TOKEN for authentication" + if [ -z "$header" ]; then + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" -O "$local_file" "$source_url" + else + wget -q --header "Authorization: Bearer $GITHUB_TOKEN" --header "$header" -O "$local_file" "$source_url" + fi + else + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi + fi +} +github_http_download() { + log_debug "github_http_download $2" + if is_command curl; then + github_http_download_curl "$@" + return + elif is_command wget; then + github_http_download_wget "$@" + return + fi + log_crit "github_http_download unable to find wget or curl" + return 1 +} +github_http_copy() { + tmp=$(mktemp) + github_http_download "${tmp}" "$@" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(github_http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} + +# --- Embedded Checksums (Format: VERSION:FILENAME:HASH) --- +EMBEDDED_CHECKSUMS="" + +# Find embedded checksum for a given version and filename +find_embedded_checksum() { + version="$1" + filename="$2" + echo "$EMBEDDED_CHECKSUMS" | grep -E "^${version}:${filename}:" | cut -d':' -f3 +} +parse_args() { + BINDIR="${BINSTALLER_BIN:-${HOME}/.local/bin}" + DRY_RUN=0 + while getopts "b:dqh?xn" arg; do + case "$arg" in + b) BINDIR="$OPTARG" ;; + d) log_set_priority 10 ;; + q) log_set_priority 3 ;; + h | \?) usage "$0" ;; + x) set -x ;; + n) DRY_RUN=1 ;; + esac + done + shift $((OPTIND - 1)) + TAG="${1:-latest}" +} +tag_to_version() { + if [ "$TAG" = "latest" ]; then + log_info "checking GitHub for latest tag" + REALTAG=$(github_release "${REPO}" "${TAG}") && true + test -n "$REALTAG" || { + log_crit "Could not determine latest tag for ${REPO}" exit 1 + } + else + # Assume TAG is a valid tag/version string + REALTAG="$TAG" + fi + if test -z "$REALTAG"; then + log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${REPO}/releases for details" + exit 1 + fi + VERSION=${REALTAG#v} # Strip leading 'v' + TAG="$REALTAG" # Use the resolved tag + log_info "Resolved version: ${VERSION} (tag: ${TAG})" +} + + + +resolve_asset_filename() { + + # --- Apply Rules --- + ASSET_FILENAME="" + if [ -z "${ASSET_FILENAME}" ]; then + ASSET_FILENAME="eds-${OS}-${ARCH}${EXT}" + fi +} +# Cleanup function to remove temporary files and stop progress +cleanup() { + # Stop progress animation + progress_clear + + if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then + log_debug "Cleaning up temporary directory: $TMPDIR" + rm -rf -- "$TMPDIR" + fi +} + +execute() { + STRIP_COMPONENTS=0 + CHECKSUM_FILENAME="checksums.txt" + + # --- Construct URLs --- + GITHUB_DOWNLOAD="https://github.com/${REPO}/releases/download" + ASSET_URL="${GITHUB_DOWNLOAD}/${TAG}/${ASSET_FILENAME}" + CHECKSUM_URL="" + if [ -n "$CHECKSUM_FILENAME" ]; then + CHECKSUM_URL="${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM_FILENAME}" + fi + + # --- Download and Verify --- + TMPDIR=$(mktemp -d) + # Set up cleanup trap that includes progress clearing + trap cleanup EXIT HUP INT TERM + log_debug "Downloading files into ${TMPDIR}" + log_info "Downloading ${ASSET_URL}" + github_http_download "${TMPDIR}/${ASSET_FILENAME}" "${ASSET_URL}" + + # Try to find embedded checksum first + EMBEDDED_HASH=$(find_embedded_checksum "$VERSION" "$ASSET_FILENAME") + + if [ -n "$EMBEDDED_HASH" ]; then + log_info "Using embedded checksum for verification" + + # Verify using embedded hash + got=$(hash_compute "${TMPDIR}/${ASSET_FILENAME}") + if [ "$got" != "$EMBEDDED_HASH" ]; then + log_crit "Checksum verification failed for ${ASSET_FILENAME}" + log_crit "Expected: ${EMBEDDED_HASH}" + log_crit "Got: ${got}" + return 1 fi + log_info "Checksum verification successful" + elif [ -n "$CHECKSUM_URL" ]; then + # Fall back to downloading checksum file + log_info "Downloading checksums from ${CHECKSUM_URL}" + github_http_download "${TMPDIR}/${CHECKSUM_FILENAME}" "${CHECKSUM_URL}" + log_info "Verifying checksum ..." + hash_verify "${TMPDIR}/${ASSET_FILENAME}" "${TMPDIR}/${CHECKSUM_FILENAME}" + else + log_info "No checksum found, skipping verification." fi - URL="${BASE_URL}/${VERSION}/${ASSET}" -else - if [[ "$VERSION" == "latest" ]]; then - URL="https://github.com/${REPO}/releases/latest/download/${ASSET}" + + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + log_debug "Target is raw binary" else - URL="https://github.com/${REPO}/releases/download/${VERSION}/${ASSET}" + log_info "Extracting ${ASSET_FILENAME}..." + (cd "${TMPDIR}" && untar "${ASSET_FILENAME}" "${STRIP_COMPONENTS}") + fi + BINARY_NAME='eds' + if [ -z "${EXT}" ] || [ "${EXT}" = ".exe" ]; then + BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}" + else + BINARY_PATH="${TMPDIR}/${ASSET_FILENAME}" fi -fi -log "Downloading $URL" + if [ "${UNAME_OS}" = "windows" ]; then + case "${BINARY_NAME}" in *.exe) ;; *) BINARY_NAME="${BINARY_NAME}.exe" ;; esac + case "${BINARY_PATH}" in *.exe) ;; *) BINARY_PATH="${BINARY_PATH}.exe" ;; esac + fi -mkdir -p "$INSTALL_DIR" -TMP="$(mktemp -t eds-cli.XXXXXX)" -trap 'rm -f "$TMP"' EXIT + if [ ! -f "${BINARY_PATH}" ]; then + log_crit "Binary not found: ${BINARY_PATH}" + log_crit "Listing contents of ${TMPDIR} ..." + if command -v find >/dev/null 2>&1; then + cd "${TMPDIR}" && find . + else + cd "${TMPDIR}" && ls -R . + fi + return 1 + fi -if command -v curl >/dev/null 2>&1; then - curl -fsSL --retry 3 -o "$TMP" "$URL" -elif command -v wget >/dev/null 2>&1; then - wget -q -O "$TMP" "$URL" -else - echo "Neither curl nor wget is available" >&2 - exit 1 -fi + progress_clear + # Install the binary + INSTALL_PATH="${BINDIR}/${BINARY_NAME}" -chmod +x "$TMP" + if [ "$DRY_RUN" = "1" ]; then + log_info "[DRY RUN] ${BINARY_NAME} dry-run installation succeeded! (Would install to: ${INSTALL_PATH})" + else + log_info "Installing binary to ${INSTALL_PATH}" + test ! -d "${BINDIR}" && install -d "${BINDIR}" + install "${BINARY_PATH}" "${INSTALL_PATH}" + log_info "${BINARY_NAME} installation complete!" + fi +} -DEST="${INSTALL_DIR}/${BIN_NAME}${EXT}" -mv "$TMP" "$DEST" +# --- Configuration --- +NAME='eds' +REPO='cloud-ru/evolution-devservices-cli' +EXT='' -# -- verify ------------------------------------------------------------------ +# use in logging routines +log_prefix() { + echo "${REPO}" +} -log "Installed to ${DEST}" -if ! "$DEST" version >/dev/null 2>&1; then - echo "Installed binary failed to report version" >&2 - exit 1 -fi -log "Verified: $($DEST version 2>/dev/null || echo unknown)" +parse_args "$@" -# -- PATH hint --------------------------------------------------------------- +progress_init +progress_pulse_start -case ":$PATH:" in - *":${INSTALL_DIR}:"*) ;; - *) - log "Add to PATH: export PATH=\"${INSTALL_DIR}:\$PATH\"" - ;; -esac +# --- Determine target platform --- +OS="${BINSTALLER_OS:-$(uname_os)}" +UNAME_OS="${OS}" + +ARCH="${BINSTALLER_ARCH:-$(uname_arch)}" + +log_info "Detected Platform: ${OS}/${ARCH}" + +# --- Validate platform --- +uname_os_check "$OS" +uname_arch_check "$ARCH" + +tag_to_version -echo "${DEST}" +resolve_asset_filename +execute