From d1fdd6e186efc43d3c79eccc319a220d45b25ec5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 13 May 2026 13:17:07 +0100 Subject: [PATCH] fix(installer): honor git install versions --- CHANGELOG.md | 1 + pnpm-workspace.yaml | 1 + scripts/install-cli.sh | 138 +++++++++++++++++++++++++++++-- scripts/install.sh | 132 +++++++++++++++++++++++++++-- test/scripts/install-cli.test.ts | 59 +++++++++++++ test/scripts/install-sh.test.ts | 40 +++++++++ 6 files changed, 357 insertions(+), 14 deletions(-) create mode 100644 test/scripts/install-cli.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a99647aff5..e99ac8f90aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai - Anthropic: reseed Claude CLI fresh-session retries from bounded OpenClaw transcript history after session rotation, preventing conversation amnesia. Fixes #80905. (#80934) Thanks @bitloi. - Require explicit browser device pairing [AI]. (#81289) Thanks @pgondhi987. - Require Control UI pairing before proxy-scoped access [AI]. (#81288) Thanks @pgondhi987. +- Installer: honor `--version` for git installs and install from the checked-in lockfile, preventing recent dependency pins from tripping pnpm's minimum-release-age gate during tag installs. - Harden trusted-proxy source validation [AI]. (#81290) Thanks @pgondhi987. - Agents: add permissive item schemas to array tool parameters before provider submission, preventing OpenAI-compatible schema validation from rejecting plugin tools that omit `items`. Fixes #81175. (#81217) Thanks @JARVIS-Glasses. - Agents: escalate LLM idle watchdog timeouts through profile rotation and configured model fallback instead of leaving agent turns stuck after a silent model stream. Fixes #76877. (#80449) Thanks @jimdawdy-hub. diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3a50f9ce38a..4916b3eadb0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -23,6 +23,7 @@ minimumReleaseAgeExclude: - "@mariozechner/*" - "@openai/codex" - "@openai/codex-*" + - "@smithy/shared-ini-file-loader@4.5.1" - "@typescript/native-preview*" - "@types/node" - "@rolldown/*" diff --git a/scripts/install-cli.sh b/scripts/install-cli.sh index 4e10389ed89..40ef46e7a46 100755 --- a/scripts/install-cli.sh +++ b/scripts/install-cli.sh @@ -364,6 +364,128 @@ run_pnpm() { "${PNPM_CMD[@]}" "$@" } +resolve_git_openclaw_ref() { + local requested="${OPENCLAW_VERSION:-latest}" + local resolved_version="" + + case "$requested" in + ""|latest) + resolved_version="$("$(npm_bin)" view "openclaw" "dist-tags.${requested:-latest}" 2>/dev/null || true)" + if [[ -n "$resolved_version" ]]; then + echo "v${resolved_version}" + return 0 + fi + echo "main" + return 0 + ;; + next|beta) + resolved_version="$("$(npm_bin)" view "openclaw" "dist-tags.${requested:-latest}" 2>/dev/null || true)" + if [[ -n "$resolved_version" ]]; then + echo "v${resolved_version}" + return 0 + fi + echo "$requested" + return 0 + ;; + main) + echo "main" + return 0 + ;; + v[0-9]*) + echo "$requested" + return 0 + ;; + [0-9]*.[0-9]*.[0-9]*) + echo "v${requested}" + return 0 + ;; + *) + echo "$requested" + return 0 + ;; + esac +} + +checkout_git_openclaw_ref() { + local repo_dir="$1" + local ref="$2" + + if [[ -z "$ref" ]]; then + return 0 + fi + + git -C "$repo_dir" fetch --tags origin + + if [[ "$ref" == "main" ]]; then + git -C "$repo_dir" checkout main + if [[ "$GIT_UPDATE" == "1" ]]; then + git -C "$repo_dir" pull --rebase || true + fi + return 0 + fi + + if git -C "$repo_dir" rev-parse --verify --quiet "refs/tags/${ref}^{commit}" >/dev/null; then + git -C "$repo_dir" checkout --detach "$ref" + return 0 + fi + + if git -C "$repo_dir" ls-remote --exit-code --heads origin "$ref" >/dev/null 2>&1; then + git -C "$repo_dir" checkout -B "$ref" "origin/$ref" + if [[ "$GIT_UPDATE" == "1" ]]; then + git -C "$repo_dir" pull --rebase || true + fi + return 0 + fi + + if git -C "$repo_dir" rev-parse --verify --quiet "${ref}^{commit}" >/dev/null; then + git -C "$repo_dir" checkout --detach "$ref" + return 0 + fi + + fail "Requested git version not found: ${ref}" +} + +repo_pnpm_spec() { + local repo_dir="$1" + local package_json="${repo_dir}/package.json" + + if [[ ! -f "$package_json" ]]; then + return 1 + fi + + sed -n -E 's/^[[:space:]]*"packageManager"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$package_json" | head -n1 +} + +activate_repo_pnpm_version() { + local repo_dir="$1" + local spec + local version + local corepack_cmd="" + + spec="$(repo_pnpm_spec "$repo_dir" || true)" + if [[ "$spec" != pnpm@* ]]; then + return 0 + fi + + version="${spec#pnpm@}" + version="${version%%+*}" + if [[ -z "$version" ]]; then + return 0 + fi + + if [[ -x "$(node_dir)/bin/corepack" ]]; then + corepack_cmd="$(node_dir)/bin/corepack" + elif command -v corepack >/dev/null 2>&1; then + corepack_cmd="$(command -v corepack)" + fi + + if [[ -n "$corepack_cmd" ]]; then + log "Activating repo pnpm ${version}" + "$corepack_cmd" prepare "pnpm@${version}" --activate >/dev/null 2>&1 || true + detect_pnpm_cmd || true + fi +} + install_node() { local os local arch @@ -588,18 +710,20 @@ install_openclaw_from_git() { git clone "$repo_url" "$repo_dir" fi - if [[ "$GIT_UPDATE" == "1" ]]; then - if [[ -z "$(git -C "$repo_dir" status --porcelain 2>/dev/null || true)" ]]; then - git -C "$repo_dir" pull --rebase || true - else - log "Repo is dirty; skipping git pull" - fi + local git_ref + git_ref="$(resolve_git_openclaw_ref)" + if [[ -z "$(git -C "$repo_dir" status --porcelain 2>/dev/null || true)" ]]; then + log "Using git ref: ${git_ref}" + checkout_git_openclaw_ref "$repo_dir" "$git_ref" + else + log "Repo is dirty; skipping git checkout/update" fi cleanup_legacy_submodules "$repo_dir" ensure_pnpm_git_prepare_allowlist "$repo_dir" + activate_repo_pnpm_version "$repo_dir" - SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" run_pnpm -C "$repo_dir" install + SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" run_pnpm -C "$repo_dir" install --frozen-lockfile if ! run_pnpm -C "$repo_dir" ui:build; then log "UI build failed; continuing (CLI may still work)" diff --git a/scripts/install.sh b/scripts/install.sh index 3d0cbdf28ec..0eed16049ef 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1910,6 +1910,122 @@ run_pnpm() { "${PNPM_CMD[@]}" "$@" } +resolve_git_openclaw_ref() { + local requested="${OPENCLAW_VERSION:-latest}" + local resolved_version="" + + case "$requested" in + ""|latest) + resolved_version="$(npm view "openclaw" "dist-tags.${requested:-latest}" 2>/dev/null || true)" + if [[ -n "$resolved_version" ]]; then + echo "v${resolved_version}" + return 0 + fi + echo "main" + return 0 + ;; + next|beta) + resolved_version="$(npm view "openclaw" "dist-tags.${requested:-latest}" 2>/dev/null || true)" + if [[ -n "$resolved_version" ]]; then + echo "v${resolved_version}" + return 0 + fi + echo "$requested" + return 0 + ;; + main) + echo "main" + return 0 + ;; + v[0-9]*) + echo "$requested" + return 0 + ;; + [0-9]*.[0-9]*.[0-9]*) + echo "v${requested}" + return 0 + ;; + *) + echo "$requested" + return 0 + ;; + esac +} + +checkout_git_openclaw_ref() { + local repo_dir="$1" + local ref="$2" + + if [[ -z "$ref" ]]; then + return 0 + fi + + run_quiet_step "Fetching requested version" git -C "$repo_dir" fetch --tags origin + + if [[ "$ref" == "main" ]]; then + run_quiet_step "Checking out main" git -C "$repo_dir" checkout main + if [[ "$GIT_UPDATE" == "1" ]]; then + run_quiet_step "Updating repository" git -C "$repo_dir" pull --rebase || true + fi + return 0 + fi + + if git -C "$repo_dir" rev-parse --verify --quiet "refs/tags/${ref}^{commit}" >/dev/null; then + run_quiet_step "Checking out ${ref}" git -C "$repo_dir" checkout --detach "$ref" + return 0 + fi + + if git -C "$repo_dir" ls-remote --exit-code --heads origin "$ref" >/dev/null 2>&1; then + run_quiet_step "Checking out ${ref}" git -C "$repo_dir" checkout -B "$ref" "origin/$ref" + if [[ "$GIT_UPDATE" == "1" ]]; then + run_quiet_step "Updating repository" git -C "$repo_dir" pull --rebase || true + fi + return 0 + fi + + if git -C "$repo_dir" rev-parse --verify --quiet "${ref}^{commit}" >/dev/null; then + run_quiet_step "Checking out ${ref}" git -C "$repo_dir" checkout --detach "$ref" + return 0 + fi + + ui_error "Requested git version not found: ${ref}" + return 1 +} + +repo_pnpm_spec() { + local repo_dir="$1" + local package_json="${repo_dir}/package.json" + + if [[ ! -f "$package_json" ]]; then + return 1 + fi + + sed -n -E 's/^[[:space:]]*"packageManager"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$package_json" | head -n1 +} + +activate_repo_pnpm_version() { + local repo_dir="$1" + local spec version + + spec="$(repo_pnpm_spec "$repo_dir" || true)" + if [[ "$spec" != pnpm@* ]]; then + return 0 + fi + + version="${spec#pnpm@}" + version="${version%%+*}" + if [[ -z "$version" ]]; then + return 0 + fi + + if command -v corepack >/dev/null 2>&1; then + ui_info "Activating repo pnpm ${version}" + corepack prepare "pnpm@${version}" --activate >/dev/null 2>&1 || true + refresh_shell_command_cache + detect_pnpm_cmd || true + fi +} + ensure_user_local_bin_on_path() { local target="$HOME/.local/bin" mkdir -p "$target" @@ -2240,17 +2356,19 @@ install_openclaw_from_git() { run_quiet_step "Cloning OpenClaw" git clone "$repo_url" "$repo_dir" fi - if [[ "$GIT_UPDATE" == "1" ]]; then - if [[ -z "$(git -C "$repo_dir" status --porcelain 2>/dev/null || true)" ]]; then - run_quiet_step "Updating repository" git -C "$repo_dir" pull --rebase || true - else - ui_info "Repo has local changes; skipping git pull" - fi + local git_ref + git_ref="$(resolve_git_openclaw_ref)" + if [[ -z "$(git -C "$repo_dir" status --porcelain 2>/dev/null || true)" ]]; then + ui_info "Using git ref: ${git_ref}" + checkout_git_openclaw_ref "$repo_dir" "$git_ref" + else + ui_info "Repo has local changes; skipping git checkout/update" fi cleanup_legacy_submodules "$repo_dir" + activate_repo_pnpm_version "$repo_dir" - SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" run_quiet_step "Installing dependencies" run_pnpm -C "$repo_dir" install + SHARP_IGNORE_GLOBAL_LIBVIPS="$SHARP_IGNORE_GLOBAL_LIBVIPS" run_quiet_step "Installing dependencies" run_pnpm -C "$repo_dir" install --frozen-lockfile if ! run_quiet_step "Building UI" run_pnpm -C "$repo_dir" ui:build; then ui_warn "UI build failed; continuing (CLI may still work)" diff --git a/test/scripts/install-cli.test.ts b/test/scripts/install-cli.test.ts new file mode 100644 index 00000000000..0423bc366d3 --- /dev/null +++ b/test/scripts/install-cli.test.ts @@ -0,0 +1,59 @@ +import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const SCRIPT_PATH = "scripts/install-cli.sh"; + +function runInstallCliShell(script: string, env: NodeJS.ProcessEnv = {}) { + return spawnSync("bash", ["-c", script], { + encoding: "utf8", + env: { + ...process.env, + OPENCLAW_INSTALL_CLI_SH_NO_RUN: "1", + ...env, + }, + }); +} + +describe("install-cli.sh", () => { + const script = readFileSync(SCRIPT_PATH, "utf8"); + + it("resolves requested git install versions to checkout refs", () => { + const result = runInstallCliShell(` + set -euo pipefail + source "${SCRIPT_PATH}" + npm_bin() { echo npm; } + npm() { + if [[ "$1" == "view" && "$2" == "openclaw" && "$3" == "dist-tags.beta" ]]; then + printf '2026.5.12-beta.3\\n' + return 0 + fi + return 1 + } + OPENCLAW_VERSION=v2026.5.12-beta.3 + printf 'tag=%s\\n' "$(resolve_git_openclaw_ref)" + OPENCLAW_VERSION=2026.5.12-beta.3 + printf 'semver=%s\\n' "$(resolve_git_openclaw_ref)" + OPENCLAW_VERSION=beta + printf 'beta=%s\\n' "$(resolve_git_openclaw_ref)" + OPENCLAW_VERSION=main + printf 'main=%s\\n' "$(resolve_git_openclaw_ref)" + `); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("tag=v2026.5.12-beta.3"); + expect(result.stdout).toContain("semver=v2026.5.12-beta.3"); + expect(result.stdout).toContain("beta=v2026.5.12-beta.3"); + expect(result.stdout).toContain("main=main"); + }); + + it("uses frozen lockfile installs for git installs", () => { + expect(script).toContain('run_pnpm -C "$repo_dir" install --frozen-lockfile'); + }); + + it("aligns pnpm to the checked-out repo packageManager before installing", () => { + expect(script).toContain("activate_repo_pnpm_version()"); + expect(script).toContain('"$corepack_cmd" prepare "pnpm@${version}" --activate'); + expect(script).toContain('activate_repo_pnpm_version "$repo_dir"'); + }); +}); diff --git a/test/scripts/install-sh.test.ts b/test/scripts/install-sh.test.ts index 7104d0e64d2..040c4ece767 100644 --- a/test/scripts/install-sh.test.ts +++ b/test/scripts/install-sh.test.ts @@ -384,6 +384,46 @@ describe("install.sh", () => { expect(result?.stdout).toContain(`missing=${openclawBin.replace(/ /g, "\\ ")}`); expect(result?.stdout).toContain("present=openclaw"); }); + + it("resolves requested git install versions to checkout refs", () => { + const result = runInstallShell(` + set -euo pipefail + source "${SCRIPT_PATH}" + npm() { + if [[ "$1" == "view" && "$2" == "openclaw" && "$3" == "dist-tags.beta" ]]; then + printf '2026.5.12-beta.3\\n' + return 0 + fi + return 1 + } + OPENCLAW_VERSION=v2026.5.12-beta.3 + printf 'tag=%s\\n' "$(resolve_git_openclaw_ref)" + OPENCLAW_VERSION=2026.5.12-beta.3 + printf 'semver=%s\\n' "$(resolve_git_openclaw_ref)" + OPENCLAW_VERSION=beta + printf 'beta=%s\\n' "$(resolve_git_openclaw_ref)" + OPENCLAW_VERSION=main + printf 'main=%s\\n' "$(resolve_git_openclaw_ref)" + `); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("tag=v2026.5.12-beta.3"); + expect(result.stdout).toContain("semver=v2026.5.12-beta.3"); + expect(result.stdout).toContain("beta=v2026.5.12-beta.3"); + expect(result.stdout).toContain("main=main"); + }); + + it("uses frozen lockfile installs for git installs", () => { + expect(script).toContain( + 'run_quiet_step "Installing dependencies" run_pnpm -C "$repo_dir" install --frozen-lockfile', + ); + }); + + it("aligns pnpm to the checked-out repo packageManager before installing", () => { + expect(script).toContain("activate_repo_pnpm_version()"); + expect(script).toContain('corepack prepare "pnpm@${version}" --activate'); + expect(script).toContain('activate_repo_pnpm_version "$repo_dir"'); + }); }); describe("install.sh macOS Homebrew Node behavior", () => {