Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 0 additions & 31 deletions .github/workflows/test.yml

This file was deleted.

248 changes: 248 additions & 0 deletions mysql/install-mysql-cli-linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
#!/bin/bash

# GameAP MySQL CLI installation script for Linux.
# Downloads the gameap-mysql release binary and prepares its state directory.
#
# Works both as root (system-wide: /usr/local/bin + /var/lib/gameap-mysql)
# and from a rootless GameAP setup (non-root gameap-daemon): without write
# access to /usr/local/bin the binary is installed next to this script —
# the daemon tools directory, which gameap-daemon puts on its PATH — and the
# state directory falls back to the user state dir the CLI resolves itself
# (${XDG_STATE_HOME:-$HOME/.local/state}/gameap-mysql).
#
# Invoked by the panel's MySQL plugin as a daemon task chain:
# get-tool .../mysql/install-mysql-cli-linux.sh
# install-mysql-cli-linux.sh --version=X.Y.Z

set -e

GAMEAP_MYSQL_VERSION="0.1.0"
DOWNLOAD_BASE=""
INSTALL_DIR=""
STATE_DIR=""

show_help() {
echo "GameAP MySQL CLI installation script"
echo
echo "Usage: $0 [options]"
echo
echo "Options:"
echo " --version=VERSION CLI version to install (default: ${GAMEAP_MYSQL_VERSION})"
echo " --install-dir=DIR Binary directory (default: /usr/local/bin when writable,"
echo " otherwise the directory of this script, then ~/.local/bin)"
echo " --state-dir=DIR State directory (default: /var/lib/gameap-mysql as root,"
echo " otherwise \${XDG_STATE_HOME:-\$HOME/.local/state}/gameap-mysql;"
echo " a custom value must also be exported to the daemon as"
echo " GAMEAP_MYSQL_STATE_DIR or the CLI will not find it)"
echo " --download-base=URL Use a single custom mirror instead of the default"
echo " GitHub/CDN mirror list; expects"
echo " URL/gameap-mysql/VERSION/gameap-mysql-VERSION-OS-ARCH"
echo " --help Show this help"
}

while [ $# -gt 0 ]; do
case "$1" in
--version=*)
GAMEAP_MYSQL_VERSION="${1#*=}"
;;
--install-dir=*)
INSTALL_DIR="${1#*=}"
;;
--state-dir=*)
STATE_DIR="${1#*=}"
;;
--download-base=*)
DOWNLOAD_BASE="${1#*=}"
;;
--help)
show_help
exit 0
;;
*)
echo "Unknown option: $1"
show_help
exit 1
;;
esac
shift
done

OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)

case $ARCH in
x86_64)
ARCH="amd64"
;;
aarch64|arm64)
ARCH="arm64"
;;
*)
echo "Unsupported architecture: $ARCH"
exit 1
;;
esac
Comment on lines +70 to +84

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add explicit OS validation.

ARCH is validated with an explicit case/exit 1 (lines 73-84), but OS (line 70) is never checked. If this script runs on a non-Linux platform, it silently builds a BINARY_FILE name for that OS and only fails later with a generic "Failed to download" message (lines 230-234), instead of a clear "unsupported OS" error.

The review-stack context for this layer states the installer "validates the runtime platform." Add the same style of explicit check used for ARCH.

🔧 Proposed fix
 OS=$(uname -s | tr '[:upper:]' '[:lower:]')
 ARCH=$(uname -m)
+
+if [ "$OS" != "linux" ]; then
+    echo "Unsupported OS: $OS (this script installs the Linux build only)"
+    exit 1
+fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case $ARCH in
x86_64)
ARCH="amd64"
;;
aarch64|arm64)
ARCH="arm64"
;;
*)
echo "Unsupported architecture: $ARCH"
exit 1
;;
esac
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
if [ "$OS" != "linux" ]; then
echo "Unsupported OS: $OS (this script installs the Linux build only)"
exit 1
fi
case $ARCH in
x86_64)
ARCH="amd64"
;;
aarch64|arm64)
ARCH="arm64"
;;
*)
echo "Unsupported architecture: $ARCH"
exit 1
;;
esac
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mysql/install-mysql-cli-linux.sh` around lines 70 - 84, Add explicit OS
validation immediately after computing OS in the installer: accept only Linux,
and print a clear unsupported-OS message followed by exit 1 for any other value.
Keep the existing ARCH case validation and downstream download flow unchanged.


resolve_install_dir() {
if [ -n "$INSTALL_DIR" ]; then
return
fi

if [ -w /usr/local/bin ]; then
INSTALL_DIR="/usr/local/bin"
return
fi

script_dir=$(cd "$(dirname "$0")" && pwd)
if [ -w "$script_dir" ]; then
# get-tool drops this script into the daemon tools directory, which
# gameap-daemon prepends to its PATH — the natural rootless target.
INSTALL_DIR="$script_dir"
return
fi

INSTALL_DIR="${HOME}/.local/bin"
}
Comment on lines +86 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Distinguish the two rootless fallback targets before printing the PATH note.

resolve_install_dir has three outcomes: /usr/local/bin, script_dir (the daemon tools directory, which the header comment says gameap-daemon puts on its PATH), and ${HOME}/.local/bin (not documented anywhere as being on the daemon's PATH). The function does not record which of the last two was chosen.

Downstream, the final note (lines 246-248) only checks [ "$INSTALL_DIR" != "/usr/local/bin" ] and prints the same "gameap-daemon resolves the binary by name via its tools PATH" message for both non-root outcomes. That claim is incorrect when INSTALL_DIR falls back to ${HOME}/.local/bin}, since that directory is not the daemon tools directory. An operator relying on this message could believe the daemon will find the binary when it will not.

Track which fallback branch is taken so the downstream message can differentiate the two cases.

🔧 Proposed fix
+INSTALL_DIR_IS_DAEMON_TOOLS_DIR=""
 resolve_install_dir() {
     if [ -n "$INSTALL_DIR" ]; then
         return
     fi

     if [ -w /usr/local/bin ]; then
         INSTALL_DIR="/usr/local/bin"
         return
     fi

     script_dir=$(cd "$(dirname "$0")" && pwd)
     if [ -w "$script_dir" ]; then
         # get-tool drops this script into the daemon tools directory, which
         # gameap-daemon prepends to its PATH — the natural rootless target.
         INSTALL_DIR="$script_dir"
+        INSTALL_DIR_IS_DAEMON_TOOLS_DIR="1"
         return
     fi

     INSTALL_DIR="${HOME}/.local/bin"
 }

Then at lines 246-248:

-if [ "$INSTALL_DIR" != "/usr/local/bin" ]; then
-    echo "Note: rootless install — gameap-daemon resolves the binary by name via its tools PATH."
-fi
+if [ "$INSTALL_DIR" = "/usr/local/bin" ] || [ -n "$INSTALL_DIR_IS_DAEMON_TOOLS_DIR" ]; then
+    :
+else
+    echo "Note: rootless install to ${INSTALL_DIR}, which is not on gameap-daemon's tools PATH."
+    echo "Add it to PATH, or re-run with --install-dir set to the daemon tools directory."
+fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mysql/install-mysql-cli-linux.sh` around lines 86 - 105, Update
resolve_install_dir to record whether the selected rootless directory is the
daemon tools directory or the separate ${HOME}/.local/bin fallback, using a
distinct state variable for the two branches. Update the final PATH note to
check that state and only claim gameap-daemon resolves the binary through its
tools PATH when script_dir was selected; provide the appropriate guidance for
the home-local fallback.


# Mirrors the CLI's own state-dir resolution (internal/platform): root uses
# the system directory; a non-root daemon uses it only when pre-provisioned
# writable, otherwise the XDG user state directory.
resolve_state_dir() {
if [ -n "$STATE_DIR" ]; then
return
fi

if [ "$(id -u)" -eq 0 ]; then
STATE_DIR="/var/lib/gameap-mysql"
return
fi

if [ -d /var/lib/gameap-mysql ] && [ -w /var/lib/gameap-mysql ]; then
STATE_DIR="/var/lib/gameap-mysql"
return
fi

STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/gameap-mysql"
}

_url_host() {
local host="${1#*://}"
echo "${host%%/*}"
}

# Measure HTTPS response latency (seconds) to a URL with a HEAD request.
# HTTP probing is used instead of ICMP ping on purpose: ping may be missing
# on minimal systems, ICMP is often filtered, and ICMP reachability does not
# imply HTTPS reachability (which is exactly why the mirrors exist). If a
# mirror ever stops answering HEAD, switch to a ranged GET: -r 0-0 instead of -I.
_probe_mirror_latency() {
curl -fsIL --connect-timeout 5 --max-time 10 -o /dev/null -w '%{time_total}' "$1"
}

# Probe all given mirror URLs in parallel and store them in the global
# ordered_mirrors array, fastest first. Mirrors that fail the probe are
# appended at the end in the given order instead of being dropped: a HEAD
# failure does not always mean a GET would fail.
ordered_mirrors=()
_order_mirrors() {
local urls=("$@")
ordered_mirrors=()

_probe_dir="$(mktemp -d -t gameap-mysql.XXXXXX)"

local i
for i in "${!urls[@]}"; do
(
local t
t="$(_probe_mirror_latency "${urls[$i]}")" \
&& printf '%s %s\n' "${t}" "${urls[$i]}" > "${_probe_dir}/${i}"
) &
done
# A bare `wait` always returns 0, so it is safe under set -e. Failed
# probes are detected by their missing result file, not by exit status.
wait

# curl always prints %{time_total} with a '.' decimal separator, but
# sort -n would misread '.' in locales whose separator is ','.
local line url
while read -r line; do
[[ -n "${line}" ]] || continue
url="${line#* }"
echo " $(_url_host "${url}"): ${line%% *}s"
ordered_mirrors+=("${url}")
done < <(LC_ALL=C sort -n "${_probe_dir}"/* 2>/dev/null)

local reachable=${#ordered_mirrors[@]}
for i in "${!urls[@]}"; do
if [[ ! -e "${_probe_dir}/${i}" ]]; then
echo " $(_url_host "${urls[$i]}"): no response, kept as a fallback"
ordered_mirrors+=("${urls[$i]}")
fi
done

if [[ "${reachable}" -eq 0 ]]; then
echo "No mirror answered the probe, mirrors will be tried in the default order."
fi

rm -rf "${_probe_dir}"
_probe_dir=""
}

resolve_install_dir
resolve_state_dir

BINARY_FILE="gameap-mysql-${GAMEAP_MYSQL_VERSION}-${OS}-${ARCH}"

# GitHub is the canonical source; the CDN mirrors keep the installation
# working where GitHub is slow or unreachable.
mirror_urls=(
"https://github.com/gameap/gameap-mysql/releases/download/${GAMEAP_MYSQL_VERSION}/${BINARY_FILE}"
"https://cdn.gameap.com/gameap-mysql/${GAMEAP_MYSQL_VERSION}/${BINARY_FILE}"
"https://cdn.gameap.ru/gameap-mysql/${GAMEAP_MYSQL_VERSION}/${BINARY_FILE}"
)

if [ -n "$DOWNLOAD_BASE" ]; then
mirror_urls=("${DOWNLOAD_BASE}/gameap-mysql/${GAMEAP_MYSQL_VERSION}/${BINARY_FILE}")
fi

if [ "${#mirror_urls[@]}" -gt 1 ]; then
echo "Choosing the fastest gameap-mysql download mirror..."
_order_mirrors "${mirror_urls[@]}"
else
ordered_mirrors=("${mirror_urls[@]}")
fi

TMP_FILE=$(mktemp /tmp/gameap-mysql.XXXXXX)
trap 'rm -f "$TMP_FILE"' EXIT

echo "Downloading gameap-mysql v${GAMEAP_MYSQL_VERSION} (${OS}-${ARCH})..."

downloaded=""
for mirror_url in "${ordered_mirrors[@]}"; do
echo "Downloading from $(_url_host "${mirror_url}")..."
if curl -fsSL --connect-timeout 10 -o "$TMP_FILE" "${mirror_url}"; then
downloaded="1"
break
fi
echo "Failed to download from ${mirror_url}, trying the next mirror..." >&2
done

if [[ -z "${downloaded}" ]]; then
echo "Failed to download gameap-mysql. Mirrors tried:" >&2
printf ' - %s\n' "${ordered_mirrors[@]}" >&2
exit 1
fi
Comment on lines +215 to +234

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a total time limit to the download request.

The probe call (_probe_mirror_latency, line 139) sets both --connect-timeout 5 and --max-time 10. The actual download at line 223 sets only --connect-timeout 10. --connect-timeout bounds connection setup only; a mirror that accepts the connection but stalls or trickles data mid-transfer can hang this call indefinitely, blocking the whole install (and the daemon task chain that invokes it), since sequential fallback to the next mirror never triggers.

Add --max-time to the download call as well.

🔧 Proposed fix
-    if curl -fsSL --connect-timeout 10 -o "$TMP_FILE" "${mirror_url}"; then
+    if curl -fsSL --connect-timeout 10 --max-time 120 -o "$TMP_FILE" "${mirror_url}"; then
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
TMP_FILE=$(mktemp /tmp/gameap-mysql.XXXXXX)
trap 'rm -f "$TMP_FILE"' EXIT
echo "Downloading gameap-mysql v${GAMEAP_MYSQL_VERSION} (${OS}-${ARCH})..."
downloaded=""
for mirror_url in "${ordered_mirrors[@]}"; do
echo "Downloading from $(_url_host "${mirror_url}")..."
if curl -fsSL --connect-timeout 10 -o "$TMP_FILE" "${mirror_url}"; then
downloaded="1"
break
fi
echo "Failed to download from ${mirror_url}, trying the next mirror..." >&2
done
if [[ -z "${downloaded}" ]]; then
echo "Failed to download gameap-mysql. Mirrors tried:" >&2
printf ' - %s\n' "${ordered_mirrors[@]}" >&2
exit 1
fi
TMP_FILE=$(mktemp /tmp/gameap-mysql.XXXXXX)
trap 'rm -f "$TMP_FILE"' EXIT
echo "Downloading gameap-mysql v${GAMEAP_MYSQL_VERSION} (${OS}-${ARCH})..."
downloaded=""
for mirror_url in "${ordered_mirrors[@]}"; do
echo "Downloading from $(_url_host "${mirror_url}")..."
if curl -fsSL --connect-timeout 10 --max-time 120 -o "$TMP_FILE" "${mirror_url}"; then
downloaded="1"
break
fi
echo "Failed to download from ${mirror_url}, trying the next mirror..." >&2
done
if [[ -z "${downloaded}" ]]; then
echo "Failed to download gameap-mysql. Mirrors tried:" >&2
printf ' - %s\n' "${ordered_mirrors[@]}" >&2
exit 1
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mysql/install-mysql-cli-linux.sh` around lines 215 - 234, Update the curl
invocation in the mirror download loop to include a finite --max-time limit,
alongside the existing --connect-timeout option, so stalled transfers eventually
fail and fallback proceeds to the next mirror.


mkdir -p "$INSTALL_DIR"
install -m 0755 "$TMP_FILE" "${INSTALL_DIR}/gameap-mysql"

install -d -m 0700 "$STATE_DIR"

echo "Verifying installation..."
"${INSTALL_DIR}/gameap-mysql" version --json

echo "gameap-mysql v${GAMEAP_MYSQL_VERSION} installed to ${INSTALL_DIR}/gameap-mysql (state: ${STATE_DIR})"

if [ "$INSTALL_DIR" != "/usr/local/bin" ]; then
echo "Note: rootless install — gameap-daemon resolves the binary by name via its tools PATH."
fi
Comment on lines +236 to +248

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

# Example verification pattern once a checksum is published alongside the binary
curl -fsSL --connect-timeout 10 --max-time 30 -o "${TMP_FILE}.sha256" "${mirror_url}.sha256"
echo "$(cat "${TMP_FILE}.sha256")  ${TMP_FILE}" | sha256sum -c -

Repository: gameap/scripts

Length of output: 288


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- installer context ---'
sed -n '1,280p' mysql/install-mysql-cli-linux.sh

printf '%s\n' '--- release and checksum references ---'
rg -n -i 'gameap-mysql|sha256|checksum|checksums|signature|cosign|gpg|release|mirror|cdn' \
  --glob '!*.min.*' --glob '!node_modules/**' . || true

printf '%s\n' '--- repository files related to release automation ---'
git ls-files | rg -i '(^|/)(release|publish|build|package|artifact|workflow)|mysql|checksum|sha256|sig|\.github' || true

Repository: gameap/scripts

Length of output: 14440


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- related repository references ---'
rg -n -i 'gameap-mysql|sha256|checksum|checksums|signature|cosign|gpg|release|mirror|cdn' \
  --glob '!*.min.*' --glob '!node_modules/**' . || true

Repository: gameap/gameap.github.io

Length of output: 2259


🏁 Script executed (no clone):

#!/bin/bash
set -u

printf '%s\n' '--- public release metadata ---'
curl -fsSL --connect-timeout 10 --max-time 30 \
  https://api.github.com/repos/gameap/gameap/releases?per_page=10 \
  | jq -r '.[] | [.tag_name, .name, (.assets[]?.name // ""), (.assets[]?.browser_download_url // "")] | `@tsv`' || true

printf '%s\n' '--- public repository metadata ---'
curl -fsSL --connect-timeout 10 --max-time 30 \
  https://api.github.com/repos/gameap/scripts/contents/mysql/install-mysql-cli-linux.sh \
  | jq -r '.download_url, .html_url' || true

Length of output: 18901


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- installer-specific URLs and artifact handling ---'
rg -n -C 8 'TMP_FILE|mirror_url|curl|wget|GAMEAP_MYSQL_VERSION|INSTALL_DIR|STATE_DIR|install ' \
  mysql/install-mysql-cli-linux.sh || true

printf '%s\n' '--- workflow and release files ---'
find .github -maxdepth 3 -type f -print 2>/dev/null | sort | while read -r f; do
  printf '\n### %s\n' "$f"
  sed -n '1,260p' "$f"
done

Repository: gameap/scripts

Length of output: 8278


🏁 Script executed (no clone):

#!/bin/bash
set -u

repo='gameap/gameap-mysql'

printf '%s\n' '--- gameap-mysql releases and assets ---'
if curl -fsSL --connect-timeout 10 --max-time 30 \
  "https://api.github.com/repos/${repo}/releases?per_page=10" |
  jq -r '.[] | [.tag_name, .name, (.assets[]?.name // "")] | `@tsv`'; then
  :
else
  printf '%s\n' 'Release API request failed'
fi

printf '%s\n' '--- repository and tag metadata ---'
curl -fsSL --connect-timeout 10 --max-time 30 \
  "https://api.github.com/repos/${repo}" |
  jq -r '[.html_url, .default_branch, .archived] | `@tsv`' || true
curl -fsSL --connect-timeout 10 --max-time 30 \
  "https://api.github.com/repos/${repo}/tags?per_page=10" |
  jq -r '.[] | [.name, .commit.sha] | `@tsv`' || true

printf '%s\n' '--- expected checksum endpoints for version 0.1.0 ---'
for host in \
  'https://github.com/gameap/gameap-mysql/releases/download/0.1.0' \
  'https://cdn.gameap.com/gameap-mysql/0.1.0' \
  'https://cdn.gameap.ru/gameap-mysql/0.1.0'
do
  url="${host}/gameap-mysql-0.1.0-linux-amd64.sha256"
  status=$(curl -sS -o /tmp/gameap-mysql-checksum.$$ -w '%{http_code}' \
    --connect-timeout 10 --max-time 30 "$url" || true)
  printf '%s\t%s\t' "$status" "$url"
  if [ -s /tmp/gameap-mysql-checksum.$$ ]; then
    tr '\n' ' ' < /tmp/gameap-mysql-checksum.$$
  fi
  printf '\n'
done
rm -f /tmp/gameap-mysql-checksum.$$

Length of output: 946


Add trusted artifact integrity verification before installation

The script executes the first successful artifact from a mirror without verification. Publish a checksum or signature in a trusted release channel and verify it before install. Do not obtain the verification value only from the selected mirror.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mysql/install-mysql-cli-linux.sh` around lines 236 - 248, Update the artifact
installation flow before the install command in install-mysql-cli-linux.sh to
verify TMP_FILE against a trusted release-channel checksum or signature, sourced
independently of the selected mirror. Abort on verification failure and only
proceed to install after successful integrity validation.