Compare commits
10 Commits
5eb5404061
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| fdf8d65310 | |||
| c2140b4493 | |||
| 652cf8bd6a | |||
| 5fdc0d45e3 | |||
| 6cde90042e | |||
| 942ab7858b | |||
| d178b8da91 | |||
| 7ecb6378fe | |||
| 9496078644 | |||
| 3aab864c34 |
@@ -3,19 +3,28 @@ run-name: ${{ gitea.event.head_commit.message }}
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
- dev
|
||||||
- master
|
- master
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
dev_tests:
|
||||||
|
if: gitea.ref_name == 'dev'
|
||||||
runs-on: debian-latest
|
runs-on: debian-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Show Debian version
|
- name: Checkout
|
||||||
run: cat /etc/os-release
|
run: |
|
||||||
- name: Test Deployment
|
git clone --quiet --depth=1 --branch=${{ gitea.ref_name }} ${{ gitea.server_url }}/${{ gitea.repository }} repo
|
||||||
run: echo "Deployment test"
|
- name: Install Bats
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y bats
|
||||||
|
- name: Run Shell Tests
|
||||||
|
run: |
|
||||||
|
cd repo
|
||||||
|
bats tests/shell/git_update.bats
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
|
if: gitea.ref_name == 'master'
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
needs: test
|
|
||||||
steps:
|
steps:
|
||||||
- name: Custom Checkout
|
- name: Custom Checkout
|
||||||
env:
|
env:
|
||||||
@@ -39,6 +48,7 @@ jobs:
|
|||||||
rm .vault_pass.txt
|
rm .vault_pass.txt
|
||||||
|
|
||||||
promote_stable:
|
promote_stable:
|
||||||
|
if: gitea.ref_name == 'master'
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
needs: deploy
|
needs: deploy
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-18
|
||||||
|
- Added CLI default PHP version detection to system status (`php_default_version`) using the CLI `php` binary.
|
||||||
|
- Updated ACP System -> CLI to show the CLI default PHP path/version in the panel header with sufficiency indicator and warning tooltip.
|
||||||
|
- Simplified ACP CLI PHP selector to `php` or custom binary, and blocked saving `keyhelp-php-domain` from ACP.
|
||||||
|
- Added test coverage expectation for `php_default_version` in system status unit tests.
|
||||||
|
- Hardened `git_update.sh` PHP selection flow with clearer logging (`initial fallback`, `bootstrap read`, `final binary`).
|
||||||
|
- Added strict PHP requirement enforcement in `git_update.sh` against `composer.json` and abort on insufficient CLI PHP.
|
||||||
|
- Refactored `git_update.sh` to `main()` for source-safe testing and added Bats shell tests for resolver/requirement behavior.
|
||||||
|
- Updated Gitea CI test job to install Bats and run `tests/shell/git_update.bats`.
|
||||||
|
|
||||||
## 2026-02-12
|
## 2026-02-12
|
||||||
- Refined ACP System tab with left navigation, section-specific requirements, and CLI PHP selector.
|
- Refined ACP System tab with left navigation, section-specific requirements, and CLI PHP selector.
|
||||||
- Added CLI PHP interpreter options (php, keyhelp-php-domain, custom) with KeyHelp guidance.
|
- Added CLI PHP interpreter options (php, keyhelp-php-domain, custom) with KeyHelp guidance.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class SystemStatusController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$phpDefaultPath = $this->resolveBinary('php');
|
$phpDefaultPath = $this->resolveBinary('php');
|
||||||
|
$phpDefaultVersion = $phpDefaultPath ? $this->resolvePhpVersion($phpDefaultPath) : null;
|
||||||
$phpConfiguredPath = trim((string) Setting::where('key', 'system.php_binary')->value('value'));
|
$phpConfiguredPath = trim((string) Setting::where('key', 'system.php_binary')->value('value'));
|
||||||
$phpSelectedPath = $phpConfiguredPath ?: (PHP_BINARY ?: $phpDefaultPath);
|
$phpSelectedPath = $phpConfiguredPath ?: (PHP_BINARY ?: $phpDefaultPath);
|
||||||
$phpSelectedOk = (bool) $phpSelectedPath;
|
$phpSelectedOk = (bool) $phpSelectedPath;
|
||||||
@@ -44,6 +45,7 @@ class SystemStatusController extends Controller
|
|||||||
return response()->json([
|
return response()->json([
|
||||||
'php' => PHP_VERSION,
|
'php' => PHP_VERSION,
|
||||||
'php_default' => $phpDefaultPath,
|
'php_default' => $phpDefaultPath,
|
||||||
|
'php_default_version' => $phpDefaultVersion,
|
||||||
'php_configured' => $phpConfiguredPath ?: null,
|
'php_configured' => $phpConfiguredPath ?: null,
|
||||||
'php_selected_path' => $phpSelectedPath,
|
'php_selected_path' => $phpSelectedPath,
|
||||||
'php_selected_ok' => $phpSelectedOk,
|
'php_selected_ok' => $phpSelectedOk,
|
||||||
|
|||||||
286
git_update.sh
286
git_update.sh
@@ -3,28 +3,6 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
cd "$ROOT_DIR"
|
|
||||||
|
|
||||||
git restore -q bootstrap/cache/packages.php bootstrap/cache/services.php 2>/dev/null || true
|
|
||||||
DIRTY="$(git status --porcelain)"
|
|
||||||
DIRTY_FILTERED="$(echo "$DIRTY" | grep -vE '^( M|M ) (bootstrap/cache/(packages|services)\.php|package-lock\.json)$' || true)"
|
|
||||||
if [[ -n "$DIRTY_FILTERED" ]]; then
|
|
||||||
echo "Working tree is dirty. Please commit or stash changes before updating."
|
|
||||||
echo "$DIRTY_FILTERED"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if echo "$DIRTY" | grep -qE 'package-lock\.json'; then
|
|
||||||
echo "Warning: package-lock.json is modified. Continuing anyway."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Fetching latest refs..."
|
|
||||||
git fetch --prune --tags
|
|
||||||
|
|
||||||
echo "Checking out stable branch..."
|
|
||||||
git checkout stable
|
|
||||||
|
|
||||||
echo "Pulling latest stable..."
|
|
||||||
git pull --ff-only
|
|
||||||
|
|
||||||
resolve_php_bin() {
|
resolve_php_bin() {
|
||||||
if [[ -n "${PHP_BIN:-}" ]]; then
|
if [[ -n "${PHP_BIN:-}" ]]; then
|
||||||
@@ -59,16 +37,17 @@ resolve_configured_php_bin() {
|
|||||||
echo "keyhelp-php-domain"
|
echo "keyhelp-php-domain"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
if command -v keyhelp-php84 >/dev/null 2>&1; then
|
if [[ -x "/usr/bin/keyhelp-php-domain" ]]; then
|
||||||
echo "keyhelp-php84"
|
echo "/usr/bin/keyhelp-php-domain"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
if command -v php >/dev/null 2>&1; then
|
if [[ -x "/usr/local/bin/keyhelp-php-domain" ]]; then
|
||||||
echo "php"
|
echo "/usr/local/bin/keyhelp-php-domain"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
echo "$current"
|
echo "Configured PHP binary 'keyhelp-php-domain' was not found." >&2
|
||||||
return
|
echo "Set ACP -> System -> CLI to a working custom binary (e.g. keyhelp-php84)." >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v "$trimmed" >/dev/null 2>&1; then
|
if command -v "$trimmed" >/dev/null 2>&1; then
|
||||||
@@ -81,63 +60,204 @@ resolve_configured_php_bin() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$current"
|
echo "Configured PHP binary '$trimmed' is not executable/resolvable." >&2
|
||||||
|
echo "Set ACP -> System -> CLI to a valid command or absolute executable path." >&2
|
||||||
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
read_setting_php_bin() {
|
read_setting_php_bin() {
|
||||||
if [[ ! -f artisan ]]; then
|
if [[ ! -f artisan ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
return
|
return 0
|
||||||
fi
|
fi
|
||||||
|
echo "Using bootstrap PHP binary to read system.php_binary: $PHP_BIN" >&2
|
||||||
"$PHP_BIN" -r '
|
"$PHP_BIN" -r '
|
||||||
require "vendor/autoload.php";
|
require "vendor/autoload.php";
|
||||||
$app = require "bootstrap/app.php";
|
$app = require "bootstrap/app.php";
|
||||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||||
$value = (string) \App\Models\Setting::where("key", "system.php_binary")->value("value");
|
$value = (string) \App\Models\Setting::where("key", "system.php_binary")->value("value");
|
||||||
echo trim($value);
|
echo trim($value);
|
||||||
' 2>/dev/null || true
|
'
|
||||||
}
|
}
|
||||||
|
|
||||||
PHP_BIN="$(resolve_php_bin)"
|
enforce_php_requirement() {
|
||||||
echo "Resolved PHP binary: $PHP_BIN"
|
local bin="${1:-php}"
|
||||||
if command -v "$PHP_BIN" >/dev/null 2>&1; then
|
echo "Validating PHP requirement from composer.json with binary: $bin"
|
||||||
echo "PHP version ($PHP_BIN): $($PHP_BIN -v | head -n 1)"
|
"$bin" -r '
|
||||||
else
|
$composer = json_decode((string) file_get_contents("composer.json"), true);
|
||||||
echo "PHP binary '$PHP_BIN' not found in PATH."
|
$constraint = (string) ($composer["require"]["php"] ?? "");
|
||||||
fi
|
$current = PHP_VERSION;
|
||||||
|
|
||||||
echo "Installing PHP dependencies..."
|
if ($constraint === "") {
|
||||||
COMPOSER_BIN="$(command -v composer || true)"
|
fwrite(STDOUT, "No PHP requirement found in composer.json; skipping check.\n");
|
||||||
if [[ -z "$COMPOSER_BIN" ]]; then
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalize = static function (string $value): ?array {
|
||||||
|
if (!preg_match("/(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?/", $value, $m)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [(int) $m[1], (int) ($m[2] ?? 0), (int) ($m[3] ?? 0)];
|
||||||
|
};
|
||||||
|
|
||||||
|
$cmp = static function (array $a, array $b): int {
|
||||||
|
for ($i = 0; $i < 3; $i++) {
|
||||||
|
if ($a[$i] > $b[$i]) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ($a[$i] < $b[$i]) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
$partMin = static function (string $part) use ($normalize): ?array {
|
||||||
|
$tokens = preg_split("/\\s+/", trim($part)) ?: [];
|
||||||
|
$tokens = array_values(array_filter($tokens, static fn ($t) => $t !== ""));
|
||||||
|
|
||||||
|
foreach ($tokens as $token) {
|
||||||
|
if (str_starts_with($token, ">=")) {
|
||||||
|
$parsed = $normalize(substr($token, 2));
|
||||||
|
if ($parsed) {
|
||||||
|
return $parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($tokens as $token) {
|
||||||
|
if (str_starts_with($token, "^")) {
|
||||||
|
$parsed = $normalize(substr($token, 1));
|
||||||
|
if ($parsed) {
|
||||||
|
return $parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (str_starts_with($token, "~")) {
|
||||||
|
$parsed = $normalize(substr($token, 1));
|
||||||
|
if ($parsed) {
|
||||||
|
return $parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($tokens[0]) ? $normalize($tokens[0]) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$parts = array_values(array_filter(array_map("trim", explode("||", $constraint)), static fn ($p) => $p !== ""));
|
||||||
|
$mins = [];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$min = $partMin($part);
|
||||||
|
if ($min) {
|
||||||
|
$mins[] = $min;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$mins) {
|
||||||
|
fwrite(STDOUT, "Could not parse PHP requirement \"$constraint\"; skipping strict check.\n");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$required = array_reduce($mins, static function ($carry, $item) use ($cmp) {
|
||||||
|
if ($carry === null) {
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
return $cmp($item, $carry) < 0 ? $item : $carry;
|
||||||
|
});
|
||||||
|
|
||||||
|
$currentParts = $normalize($current);
|
||||||
|
if (!$currentParts) {
|
||||||
|
fwrite(STDERR, "Unable to parse current PHP version: $current\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredString = implode(".", $required);
|
||||||
|
if ($cmp($currentParts, $required) < 0) {
|
||||||
|
fwrite(STDERR, "PHP requirement check failed: composer.json requires \"$constraint\" (>= $requiredString), current is $current.\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fwrite(STDOUT, "PHP requirement check passed: composer.json requires \"$constraint\" (>= $requiredString), current is $current.\n");
|
||||||
|
' || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
git restore -q bootstrap/cache/packages.php bootstrap/cache/services.php 2>/dev/null || true
|
||||||
|
DIRTY="$(git status --porcelain)"
|
||||||
|
DIRTY_FILTERED="$(echo "$DIRTY" | grep -vE '^( M|M ) (bootstrap/cache/(packages|services)\.php|package-lock\.json)$' || true)"
|
||||||
|
if [[ -n "$DIRTY_FILTERED" ]]; then
|
||||||
|
echo "Working tree is dirty. Please commit or stash changes before updating."
|
||||||
|
echo "$DIRTY_FILTERED"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if echo "$DIRTY" | grep -qE 'package-lock\.json'; then
|
||||||
|
echo "Warning: package-lock.json is modified. Continuing anyway."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Fetching latest refs..."
|
||||||
|
git fetch --prune --tags
|
||||||
|
|
||||||
|
echo "Checking out stable branch..."
|
||||||
|
git checkout stable
|
||||||
|
|
||||||
|
echo "Pulling latest stable..."
|
||||||
|
git pull --ff-only
|
||||||
|
|
||||||
|
PHP_BIN="$(resolve_php_bin)"
|
||||||
|
echo "Initial fallback PHP binary: $PHP_BIN"
|
||||||
|
if command -v "$PHP_BIN" >/dev/null 2>&1; then
|
||||||
|
echo "PHP version ($PHP_BIN): $($PHP_BIN -v | head -n 1)"
|
||||||
|
else
|
||||||
|
echo "PHP binary '$PHP_BIN' not found in PATH."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installing PHP dependencies..."
|
||||||
|
COMPOSER_BIN="$(command -v composer || true)"
|
||||||
|
if [[ -z "$COMPOSER_BIN" ]]; then
|
||||||
echo "Composer not found in PATH."
|
echo "Composer not found in PATH."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
"$PHP_BIN" "$COMPOSER_BIN" install --no-dev --optimize-autoloader
|
echo "Running with PHP binary: $PHP_BIN $COMPOSER_BIN install --no-dev --optimize-autoloader"
|
||||||
|
"$PHP_BIN" "$COMPOSER_BIN" install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
CONFIGURED_PHP="$(read_setting_php_bin)"
|
if ! CONFIGURED_PHP="$(read_setting_php_bin)"; then
|
||||||
PHP_BIN="$(resolve_configured_php_bin "$CONFIGURED_PHP" "$PHP_BIN")"
|
echo "Failed to read configured PHP binary from settings." >&2
|
||||||
|
echo "Aborting to avoid running update with the wrong PHP binary." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Configured PHP binary from settings: ${CONFIGURED_PHP:-<empty>}"
|
||||||
|
PHP_BIN="$(resolve_configured_php_bin "$CONFIGURED_PHP" "$PHP_BIN")"
|
||||||
|
|
||||||
echo "Final PHP binary: $PHP_BIN"
|
echo "Final PHP binary: $PHP_BIN"
|
||||||
if command -v "$PHP_BIN" >/dev/null 2>&1; then
|
if command -v "$PHP_BIN" >/dev/null 2>&1; then
|
||||||
echo "Final PHP version ($PHP_BIN): $($PHP_BIN -v | head -n 1)"
|
echo "Final PHP version ($PHP_BIN): $($PHP_BIN -v | head -n 1)"
|
||||||
fi
|
fi
|
||||||
|
if ! enforce_php_requirement "$PHP_BIN"; then
|
||||||
|
echo "Aborting update because selected PHP binary does not satisfy composer.json requirements." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Installing JS dependencies..."
|
echo "Installing JS dependencies..."
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
echo "Building assets..."
|
echo "Building assets..."
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
echo "Running migrations..."
|
echo "Running migrations..."
|
||||||
"$PHP_BIN" artisan migrate --force
|
echo "Running with PHP binary: $PHP_BIN artisan migrate --force"
|
||||||
|
"$PHP_BIN" artisan migrate --force
|
||||||
|
|
||||||
echo "Syncing version/build to settings..."
|
echo "Syncing version/build to settings..."
|
||||||
VERSION="$("$PHP_BIN" -r '$c=json_decode(file_get_contents("composer.json"), true); echo $c["version"] ?? "";')"
|
echo "Running with PHP binary: $PHP_BIN -r <read composer.json version>"
|
||||||
BUILD="$("$PHP_BIN" -r '$c=json_decode(file_get_contents("composer.json"), true); echo $c["build"] ?? "";')"
|
VERSION="$("$PHP_BIN" -r '$c=json_decode(file_get_contents("composer.json"), true); echo $c["version"] ?? "";')"
|
||||||
echo "Computed from composer.json: VERSION=$VERSION, BUILD=$BUILD"
|
echo "Running with PHP binary: $PHP_BIN -r <read composer.json build>"
|
||||||
|
BUILD="$("$PHP_BIN" -r '$c=json_decode(file_get_contents("composer.json"), true); echo $c["build"] ?? "";')"
|
||||||
|
echo "Computed from composer.json: VERSION=$VERSION, BUILD=$BUILD"
|
||||||
|
|
||||||
if [[ -n "$VERSION" || -n "$BUILD" ]]; then
|
if [[ -n "$VERSION" || -n "$BUILD" ]]; then
|
||||||
echo "Updating settings version/build (VERSION=$VERSION, BUILD=$BUILD)..."
|
echo "Updating settings version/build (VERSION=$VERSION, BUILD=$BUILD)..."
|
||||||
|
echo "Running with PHP binary: $PHP_BIN -r <write settings version/build>"
|
||||||
SPEEDBB_VERSION="$VERSION" SPEEDBB_BUILD="$BUILD" "$PHP_BIN" -r '
|
SPEEDBB_VERSION="$VERSION" SPEEDBB_BUILD="$BUILD" "$PHP_BIN" -r '
|
||||||
require "vendor/autoload.php";
|
require "vendor/autoload.php";
|
||||||
$app = require "bootstrap/app.php";
|
$app = require "bootstrap/app.php";
|
||||||
@@ -145,26 +265,33 @@ if [[ -n "$VERSION" || -n "$BUILD" ]]; then
|
|||||||
$version = getenv("SPEEDBB_VERSION");
|
$version = getenv("SPEEDBB_VERSION");
|
||||||
$build = getenv("SPEEDBB_BUILD");
|
$build = getenv("SPEEDBB_BUILD");
|
||||||
if ($version !== false && $version !== "") {
|
if ($version !== false && $version !== "") {
|
||||||
$updated = \Illuminate\Support\Facades\DB::table("settings")
|
\Illuminate\Support\Facades\DB::table("settings")->upsert(
|
||||||
->where("key", "version")
|
[[
|
||||||
->update(["value" => $version]);
|
"key" => "version",
|
||||||
if ($updated === 0) {
|
"value" => $version,
|
||||||
\Illuminate\Support\Facades\DB::table("settings")
|
"created_at" => now(),
|
||||||
->insert(["key" => "version", "value" => $version, "created_at" => now(), "updated_at" => now()]);
|
"updated_at" => now(),
|
||||||
}
|
]],
|
||||||
echo "Updated version rows: {$updated}\n";
|
["key"],
|
||||||
|
["value", "updated_at"]
|
||||||
|
);
|
||||||
|
echo "Upserted version setting.\n";
|
||||||
}
|
}
|
||||||
if ($build !== false && $build !== "") {
|
if ($build !== false && $build !== "") {
|
||||||
$updated = \Illuminate\Support\Facades\DB::table("settings")
|
\Illuminate\Support\Facades\DB::table("settings")->upsert(
|
||||||
->where("key", "build")
|
[[
|
||||||
->update(["value" => $build]);
|
"key" => "build",
|
||||||
if ($updated === 0) {
|
"value" => $build,
|
||||||
\Illuminate\Support\Facades\DB::table("settings")
|
"created_at" => now(),
|
||||||
->insert(["key" => "build", "value" => $build, "created_at" => now(), "updated_at" => now()]);
|
"updated_at" => now(),
|
||||||
}
|
]],
|
||||||
echo "Updated build rows: {$updated}\n";
|
["key"],
|
||||||
|
["value", "updated_at"]
|
||||||
|
);
|
||||||
|
echo "Upserted build setting.\n";
|
||||||
}
|
}
|
||||||
' \
|
' \
|
||||||
|
&& echo "Running with PHP binary: $PHP_BIN -r <verify settings version/build>" \
|
||||||
&& "$PHP_BIN" -r '
|
&& "$PHP_BIN" -r '
|
||||||
require "vendor/autoload.php";
|
require "vendor/autoload.php";
|
||||||
$app = require "bootstrap/app.php";
|
$app = require "bootstrap/app.php";
|
||||||
@@ -173,6 +300,11 @@ if [[ -n "$VERSION" || -n "$BUILD" ]]; then
|
|||||||
$build = \App\Models\Setting::where("key", "build")->value("value");
|
$build = \App\Models\Setting::where("key", "build")->value("value");
|
||||||
echo "Settings now: version={$version}, build={$build}\n";
|
echo "Settings now: version={$version}, build={$build}\n";
|
||||||
'
|
'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Update complete."
|
echo "Update complete."
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||||
|
main "$@"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -299,11 +299,7 @@ function Acp({ isAdmin }) {
|
|||||||
}
|
}
|
||||||
setGeneralSettings(next)
|
setGeneralSettings(next)
|
||||||
const configuredPhp = settingsMap.get('system.php_binary') || ''
|
const configuredPhp = settingsMap.get('system.php_binary') || ''
|
||||||
const phpMode = configuredPhp === 'keyhelp-php-domain'
|
const phpMode = configuredPhp === '' || configuredPhp === 'php' ? 'php' : 'custom'
|
||||||
? 'keyhelp'
|
|
||||||
: configuredPhp === '' || configuredPhp === 'php'
|
|
||||||
? 'php'
|
|
||||||
: 'custom'
|
|
||||||
setSystemCliSettings({
|
setSystemCliSettings({
|
||||||
php_mode: phpMode,
|
php_mode: phpMode,
|
||||||
php_custom: phpMode === 'custom' ? configuredPhp : '',
|
php_custom: phpMode === 'custom' ? configuredPhp : '',
|
||||||
@@ -399,11 +395,12 @@ function Acp({ isAdmin }) {
|
|||||||
value = typeof systemCliSettings.php_custom === 'string'
|
value = typeof systemCliSettings.php_custom === 'string'
|
||||||
? systemCliSettings.php_custom.trim()
|
? systemCliSettings.php_custom.trim()
|
||||||
: String(systemCliSettings.php_custom ?? '')
|
: String(systemCliSettings.php_custom ?? '')
|
||||||
} else if (systemCliSettings.php_mode === 'keyhelp') {
|
|
||||||
value = 'keyhelp-php-domain'
|
|
||||||
} else {
|
} else {
|
||||||
value = 'php'
|
value = 'php'
|
||||||
}
|
}
|
||||||
|
if (value === 'keyhelp-php-domain') {
|
||||||
|
throw new Error('`keyhelp-php-domain` is disabled in ACP CLI settings. Use a custom binary (e.g. keyhelp-php84).')
|
||||||
|
}
|
||||||
await saveSetting('system.php_binary', value)
|
await saveSetting('system.php_binary', value)
|
||||||
setSystemCliSettings((prev) => ({
|
setSystemCliSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -417,6 +414,77 @@ function Acp({ isAdmin }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeSemver = (value) => {
|
||||||
|
if (!value) return null
|
||||||
|
const match = String(value).trim().match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
|
||||||
|
if (!match) return null
|
||||||
|
return [Number(match[1]), Number(match[2] || 0), Number(match[3] || 0)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const compareSemver = (a, b) => {
|
||||||
|
for (let i = 0; i < 3; i += 1) {
|
||||||
|
if (a[i] > b[i]) return 1
|
||||||
|
if (a[i] < b[i]) return -1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseMinPhpConstraint = (constraint) => {
|
||||||
|
if (!constraint) return null
|
||||||
|
const parts = String(constraint)
|
||||||
|
.split('||')
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
const mins = []
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
const tokens = part.split(/\s+/).filter(Boolean)
|
||||||
|
const geToken = tokens.find((token) => token.startsWith('>='))
|
||||||
|
if (geToken) {
|
||||||
|
const parsed = normalizeSemver(geToken.slice(2))
|
||||||
|
if (parsed) mins.push(parsed)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const caretToken = tokens.find((token) => token.startsWith('^'))
|
||||||
|
if (caretToken) {
|
||||||
|
const parsed = normalizeSemver(caretToken.slice(1))
|
||||||
|
if (parsed) mins.push(parsed)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const tildeToken = tokens.find((token) => token.startsWith('~'))
|
||||||
|
if (tildeToken) {
|
||||||
|
const parsed = normalizeSemver(tildeToken.slice(1))
|
||||||
|
if (parsed) mins.push(parsed)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const plain = normalizeSemver(tokens[0] || '')
|
||||||
|
if (plain) mins.push(plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mins.length) return null
|
||||||
|
return mins.reduce((lowest, current) => (compareSemver(current, lowest) < 0 ? current : lowest))
|
||||||
|
}
|
||||||
|
|
||||||
|
const cliDefaultPhpIsSufficient = useMemo(() => {
|
||||||
|
const minimum = parseMinPhpConstraint(systemStatus?.min_versions?.php)
|
||||||
|
const current = normalizeSemver(systemStatus?.php_default_version)
|
||||||
|
if (!minimum) return true
|
||||||
|
if (!current) return false
|
||||||
|
return compareSemver(current, minimum) >= 0
|
||||||
|
}, [systemStatus])
|
||||||
|
|
||||||
|
const phpSelectedIsSufficient = useMemo(() => {
|
||||||
|
if (!systemStatus?.php_selected_ok) return false
|
||||||
|
const minimum = parseMinPhpConstraint(systemStatus?.min_versions?.php)
|
||||||
|
const current = normalizeSemver(systemStatus?.php_selected_version)
|
||||||
|
if (!minimum) return true
|
||||||
|
if (!current) return false
|
||||||
|
return compareSemver(current, minimum) >= 0
|
||||||
|
}, [systemStatus])
|
||||||
|
|
||||||
const systemChecks = useMemo(() => {
|
const systemChecks = useMemo(() => {
|
||||||
if (!systemStatus) return []
|
if (!systemStatus) return []
|
||||||
return [
|
return [
|
||||||
@@ -426,7 +494,7 @@ function Acp({ isAdmin }) {
|
|||||||
path: systemStatus.php_selected_path || '—',
|
path: systemStatus.php_selected_path || '—',
|
||||||
min: systemStatus.min_versions?.php || '—',
|
min: systemStatus.min_versions?.php || '—',
|
||||||
current: systemStatus.php_selected_version || '—',
|
current: systemStatus.php_selected_version || '—',
|
||||||
status: systemStatus.php_selected_ok ? 'ok' : 'bad',
|
status: phpSelectedIsSufficient ? 'ok' : 'bad',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'composer',
|
id: 'composer',
|
||||||
@@ -504,7 +572,7 @@ function Acp({ isAdmin }) {
|
|||||||
status: systemStatus.updates_writable ? 'ok' : 'bad',
|
status: systemStatus.updates_writable ? 'ok' : 'bad',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}, [systemStatus, t])
|
}, [phpSelectedIsSufficient, systemStatus, t])
|
||||||
|
|
||||||
const visibleSystemChecks = useMemo(() => {
|
const visibleSystemChecks = useMemo(() => {
|
||||||
const visibilityBySection = {
|
const visibilityBySection = {
|
||||||
@@ -3790,6 +3858,29 @@ function Acp({ isAdmin }) {
|
|||||||
<div className="bb-acp-panel">
|
<div className="bb-acp-panel">
|
||||||
<div className="bb-acp-panel-header">
|
<div className="bb-acp-panel-header">
|
||||||
<h5 className="mb-0">CLI</h5>
|
<h5 className="mb-0">CLI</h5>
|
||||||
|
<p className="bb-muted mb-0 mt-1">
|
||||||
|
CLI default php: {systemStatus?.php_default || '—'} (
|
||||||
|
{systemStatus?.php_default_version || 'unknown'}){' '}
|
||||||
|
{cliDefaultPhpIsSufficient ? (
|
||||||
|
<i className="bi bi-check-circle-fill text-success" aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<OverlayTrigger
|
||||||
|
placement="top"
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="cli-default-php-warning" data-bs-theme="light">
|
||||||
|
You must select a custom PHP interpreter, as the system default is not sufficient.
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<i
|
||||||
|
className="bi bi-exclamation-triangle-fill text-warning"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</OverlayTrigger>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bb-acp-panel-body">
|
<div className="bb-acp-panel-body">
|
||||||
{systemCliError && <p className="text-danger">{systemCliError}</p>}
|
{systemCliError && <p className="text-danger">{systemCliError}</p>}
|
||||||
@@ -3807,7 +3898,6 @@ function Acp({ isAdmin }) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<option value="php">php (system default)</option>
|
<option value="php">php (system default)</option>
|
||||||
<option value="keyhelp">keyhelp-php-domain</option>
|
|
||||||
<option value="custom">Custom binary (e.g. keyhelp-php84)</option>
|
<option value="custom">Custom binary (e.g. keyhelp-php84)</option>
|
||||||
</Form.Select>
|
</Form.Select>
|
||||||
{systemCliSettings.php_mode === 'custom' && (
|
{systemCliSettings.php_mode === 'custom' && (
|
||||||
@@ -3824,8 +3914,9 @@ function Acp({ isAdmin }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Form.Text className="bb-muted">
|
<Form.Text className="bb-muted">
|
||||||
Used for CLI-based updates and maintenance tasks. `keyhelp-php-domain`
|
Minimum required PHP (from composer.json):{' '}
|
||||||
is available on KeyHelp Pro; on non-Pro setups use a custom binary.
|
{systemStatus?.min_versions?.php || 'unknown'}. Use a custom binary
|
||||||
|
on like php84. On KeyHelp setups use e.g. `keyhelp-php84`.
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Button type="submit" variant="dark" disabled={systemCliSaving}>
|
<Button type="submit" variant="dark" disabled={systemCliSaving}>
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ it('returns system status for admins', function (): void {
|
|||||||
expect($payload)->toHaveKeys([
|
expect($payload)->toHaveKeys([
|
||||||
'php',
|
'php',
|
||||||
'php_default',
|
'php_default',
|
||||||
|
'php_default_version',
|
||||||
'composer',
|
'composer',
|
||||||
'composer_version',
|
'composer_version',
|
||||||
'node',
|
'node',
|
||||||
|
|||||||
52
tests/shell/git_update.bats
Normal file
52
tests/shell/git_update.bats
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
|
||||||
|
SCRIPT_PATH="$REPO_ROOT/git_update.sh"
|
||||||
|
TMP_ROOT="$BATS_TEST_TMPDIR/work"
|
||||||
|
mkdir -p "$TMP_ROOT/bin"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "git_update.sh can be sourced without running main" {
|
||||||
|
run bash -lc "source '$SCRIPT_PATH' >/dev/null 2>&1; echo sourced"
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == "sourced" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "resolve_configured_php_bin accepts command name from PATH" {
|
||||||
|
cat >"$TMP_ROOT/bin/php84" <<'SH'
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
exit 0
|
||||||
|
SH
|
||||||
|
chmod +x "$TMP_ROOT/bin/php84"
|
||||||
|
|
||||||
|
run bash -lc "PATH='$TMP_ROOT/bin':\$PATH; source '$SCRIPT_PATH'; resolve_configured_php_bin 'php84' 'php'"
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "php84" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "resolve_configured_php_bin rejects unknown custom command" {
|
||||||
|
run bash -lc "PATH='$TMP_ROOT/bin'; source '$SCRIPT_PATH'; resolve_configured_php_bin 'does-not-exist' 'php'"
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
[[ "$output" == *"Configured PHP binary 'does-not-exist' is not executable/resolvable."* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "enforce_php_requirement passes for satisfied constraint" {
|
||||||
|
cat >"$TMP_ROOT/composer.json" <<'JSON'
|
||||||
|
{"require":{"php":">=8.0"}}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
run bash -lc "cd '$TMP_ROOT'; source '$SCRIPT_PATH'; enforce_php_requirement php"
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"PHP requirement check passed"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "enforce_php_requirement fails for unsatisfied constraint" {
|
||||||
|
cat >"$TMP_ROOT/composer.json" <<'JSON'
|
||||||
|
{"require":{"php":">=99.0"}}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
run bash -lc "cd '$TMP_ROOT'; source '$SCRIPT_PATH'; enforce_php_requirement php"
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
[[ "$output" == *"PHP requirement check failed"* ]]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user