Ctrl K
ComunidadGuías…

scripts/verify-install.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
# Repeatable check of the documented one-line install. Repository tooling, not
# part of the shipped binary or the release.
#
# It runs the exact one-liner,  curl -fsSL <installer URL> | bash,  in a
# throwaway HOME with SELF_HOST_INSTALL_DIR inside the sandbox, then asserts:
#   1. self-host --version equals the latest published CLI release
#   2. self-host skill works
#   3. the installer never prompts or hangs without a terminal (it runs in a
#      new session with no controlling terminal) and prints the skill hint
#   4. a pinned old version (SELF_HOST_VERSION, default 0.3.0, the oldest
#      release that finds plain vX.Y.Z tags) upgrades to the
#      latest through  self-host self-update
#
# Environment:
#   SELF_HOST_GITHUB_TOKEN   only while the repository is private. Sent as a
#                            header only to GitHub and passed to the
#                            installer; never printed. Once the repository is
#                            public, run this with no token.
#   VERIFY_INSTALL_URL       the URL to pipe into bash. Defaults to the
#                            public installer site. A file:// URL tests a local
#                            script before it is pushed.
#   SELF_HOST_RELEASE_REPO   repository that publishes the releases
#   VERIFY_PINNED_VERSION    the old version to upgrade from (default 0.3.0)
#   VERIFY_EXPECTED_VERSION  skip the release lookup and expect this version
#   VERIFY_INSTALL_TIMEOUT   seconds before an install counts as hung (default 300)
#
# Needs curl, bash, tar and python3.
set +x
set -euo pipefail

REPO="${SELF_HOST_RELEASE_REPO:-ajnart/self-host}"
URL="${VERIFY_INSTALL_URL:-https://self-host.fyi/install}"
PINNED="${VERIFY_PINNED_VERSION:-0.3.0}"
TOKEN="${SELF_HOST_GITHUB_TOKEN:-}"

sandbox="$(mktemp -d "${TMPDIR:-/tmp}/self-host-verify.XXXXXX")"
trap 'rm -rf "$sandbox"' EXIT
mkdir -p "$sandbox/home"

failures=0
pass() { printf 'PASS  %s\n' "$*"; }
fail() {
  printf 'FAIL  %s\n' "$*" >&2
  failures=$((failures + 1))
}

# A token file keeps the secret out of process arguments and the output.
curl_config="$sandbox/curl.conf"
: >"$curl_config"
chmod 600 "$curl_config"
if [ -n "$TOKEN" ]; then
  printf 'header = "Authorization: Bearer %s"\n' "$TOKEN" >"$curl_config"
fi
installer_config="$sandbox/installer.conf"
: >"$installer_config"
chmod 600 "$installer_config"
case "$URL" in
  https://raw.githubusercontent.com/*) command cp "$curl_config" "$installer_config" ;;
esac

# sandboxed runs a command with the sandbox HOME and nothing of the person's
# own login. Without a token it also hides gh, so a logged-in gh cannot make a
# public-repository run pass for the wrong reason.
sandboxed() {
  if [ -n "$TOKEN" ]; then
    env -i HOME="$sandbox/home" PATH="$PATH" SHELL=/bin/bash TMPDIR="$sandbox" \
      LANG="${LANG:-}" LC_ALL="${LC_ALL:-}" LC_CTYPE="${LC_CTYPE:-}" \
      SELF_HOST_GITHUB_TOKEN="$TOKEN" SELF_HOST_RELEASE_REPO="$REPO" "$@"
  else
    env -i HOME="$sandbox/home" PATH="$PATH" SHELL=/bin/bash TMPDIR="$sandbox" \
      LANG="${LANG:-}" LC_ALL="${LC_ALL:-}" LC_CTYPE="${LC_CTYPE:-}" \
      GH_HOST=gh.invalid SELF_HOST_RELEASE_REPO="$REPO" "$@"
  fi
}

# no_terminal.py runs a command in a new session, so /dev/tty cannot be opened,
# and kills it when it hangs. This is how CI and cron see the installer.
# PYTHONCOERCECLOCALE=0 stops Python turning the C locale into UTF-8 for it.
no_terminal="$sandbox/no_terminal.py"
cat >"$no_terminal" <<'PY'
import subprocess, sys
try:
    sys.exit(subprocess.run(sys.argv[2:], start_new_session=True, timeout=int(sys.argv[1])).returncode)
except subprocess.TimeoutExpired:
    print("timed out: the command hung", file=sys.stderr)
    sys.exit(124)
PY

# one_liner is the documented command, exactly, with the token header added
# only for a private repository, and no terminal to ask anything on.
one_liner() {
  local install_dir="$1"
  shift
  sandboxed SELF_HOST_INSTALL_DIR="$install_dir" "$@" \
    PYTHONCOERCECLOCALE=0 python3 "$no_terminal" "${VERIFY_INSTALL_TIMEOUT:-300}" \
    bash -o pipefail -c 'curl -fsSL -K "$0" "$1" | bash' "$installer_config" "$URL"
}

expected_version() {
  local metadata metadata_error metadata_url status
  if [ -n "${VERIFY_EXPECTED_VERSION:-}" ]; then
    printf '%s\n' "$VERIFY_EXPECTED_VERSION"
    return
  fi
  metadata="$sandbox/releases.json"
  metadata_error="$sandbox/releases.error"
  metadata_url="https://api.github.com/repos/${REPO}/releases?per_page=100"
  if ! status="$(curl -sS -K "$curl_config" -H 'Accept: application/vnd.github+json' \
    -o "$metadata" -w '%{http_code}' "$metadata_url" 2>"$metadata_error")"; then
    printf 'verify-install: release discovery could not reach GitHub (HTTP %s)\n' "${status:-000}" >&2
    return 1
  fi
  case "$status" in
    200) ;;
    403)
      printf 'verify-install: release discovery got HTTP 403; GitHub anonymous API rate limit reached, wait or set SELF_HOST_GITHUB_TOKEN\n' >&2
      return 1
      ;;
    404)
      printf 'verify-install: release discovery got HTTP 404; the release repository is private or missing\n' >&2
      return 1
      ;;
    *)
      printf 'verify-install: release discovery got HTTP %s\n' "$status" >&2
      return 1
      ;;
  esac
  python3 -I -c '
import json, re, sys
try:
    releases = json.load(sys.stdin)
except (json.JSONDecodeError, TypeError):
    sys.exit("verify-install: release discovery returned invalid JSON")
if not isinstance(releases, list):
    sys.exit("verify-install: release discovery returned invalid metadata")
best = None
for rel in releases:
    if not isinstance(rel, dict):
        continue
    if rel.get("draft") or rel.get("prerelease"):
        continue
    m = re.fullmatch(r"(?:new-cli-)?v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)", rel["tag_name"])
    if not m:
        continue
    version = ".".join(m.groups())
    names = {a["name"] for a in rel.get("assets", [])}
    if "checksums.txt" not in names or not any(n.startswith("self-host_%s_" % version) and n.endswith(".tar.gz") for n in names):
        continue
    key = tuple(int(g) for g in m.groups())
    if best is None or key > best[0]:
        best = (key, version)
if best is None:
    sys.exit("verify-install: release metadata has no stable CLI release")
print(best[1])' <"$metadata"
}

echo "Installer:  $URL"
echo "Repository: $REPO ($([ -n "$TOKEN" ] && echo 'with a token' || echo 'no token'))"
latest="$(expected_version)"
echo "Latest CLI release: $latest"
echo

# 1 and 2: a fresh install of the latest release.
latest_dir="$sandbox/latest/bin"
if output="$(one_liner "$latest_dir" 2>&1)"; then
  pass "curl | bash installed into the sandbox"
else
  fail "curl | bash failed:"
  printf '%s\n' "$output" | sed 's/^/      /' >&2
fi
if [ -x "$latest_dir/self-host" ]; then
  got="$(sandboxed "$latest_dir/self-host" --version 2>&1 || true)"
  if [ "$got" = "$latest" ]; then pass "self-host --version is $got, the latest release"; else fail "self-host --version is '$got', want $latest"; fi
  if sandboxed "$latest_dir/self-host" skill 2>/dev/null | grep -q .; then pass "self-host skill prints the guide"; else fail "self-host skill printed nothing or failed"; fi
  case "$output" in
    *"y/n"*) fail "the installer prompted without a terminal" ;;
    *"Install the self-host skill from github.com/${REPO} and help me set up my own home server."*) pass "no terminal: the installer did not prompt and printed the skill hint" ;;
    *) fail "no terminal: the installer printed no skill hint" ;;
  esac
  case "$output" in
    *"not on your PATH"*) pass "the installer said how to put $latest_dir on PATH" ;;
    *) fail "the installer did not explain PATH for a directory that is not on it" ;;
  esac
else
  fail "no binary at $latest_dir/self-host"
fi

# 3: a pinned old version upgrades itself.
pinned_dir="$sandbox/pinned/bin"
if [ "$PINNED" = "$latest" ]; then
  echo "SKIP  pinned $PINNED is already the latest release"
elif output="$(one_liner "$pinned_dir" SELF_HOST_VERSION="$PINNED" 2>&1)"; then
  got="$(sandboxed "$pinned_dir/self-host" --version 2>&1 || true)"
  if [ "$got" = "$PINNED" ]; then pass "SELF_HOST_VERSION=$PINNED installed $got"; else fail "pinned install reports '$got', want $PINNED"; fi
  if update="$(sandboxed "$pinned_dir/self-host" self-update 2>&1)"; then
    got="$(sandboxed "$pinned_dir/self-host" --version 2>&1 || true)"
    if [ "$got" = "$latest" ]; then pass "self-host self-update upgraded $PINNED to $got"; else fail "after self-update --version is '$got', want $latest"; fi
  else
    fail "self-host self-update failed:"
    printf '%s\n' "$update" | sed 's/^/      /' >&2
  fi
else
  fail "pinned install of $PINNED failed:"
  printf '%s\n' "$output" | sed 's/^/      /' >&2
fi

echo
if [ "$failures" -eq 0 ]; then
  echo "verify-install: all checks passed"
else
  echo "verify-install: $failures check(s) failed" >&2
  exit 1
fi