Files
Qortal-Nextcloud-Integration/scripts/check-nextcloud-custom-app-compat.sh
2026-07-02 14:47:55 -07:00

194 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
apps_dir="${repo_root}/nextcloud/custom_apps"
nextcloud_image="${NEXTCLOUD_IMAGE:-}"
list_blocking_apps="0"
filter_mode="all"
usage() {
cat <<'USAGE'
Usage: ./scripts/check-nextcloud-custom-app-compat.sh [options]
Checks custom app appinfo/info.xml Nextcloud dependency ranges against the
target Nextcloud Docker image major.
Options:
--image <image> Target Nextcloud image, for example nextcloud:34.0.1-apache
--apps-dir <path> Custom apps directory (default: ./nextcloud/custom_apps)
--list-blocking-apps Print only incompatible app IDs, one per line
--tracked-only Check only app manifests tracked by this git repo
--untracked-only Check only app manifests not tracked by this git repo
-h, --help Show this help
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--image)
nextcloud_image="${2:-}"
shift 2
;;
--image=*)
nextcloud_image="${1#*=}"
shift
;;
--apps-dir)
apps_dir="${2:-}"
shift 2
;;
--apps-dir=*)
apps_dir="${1#*=}"
shift
;;
--list-blocking-apps)
list_blocking_apps="1"
shift
;;
--tracked-only)
filter_mode="tracked"
shift
;;
--untracked-only)
filter_mode="untracked"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1"
usage
exit 1
;;
esac
done
if [[ -z "${nextcloud_image}" ]]; then
echo "[nextcloud-app-compat] Skipped: --image or NEXTCLOUD_IMAGE is required."
exit 0
fi
if [[ ! -d "${apps_dir}" ]]; then
echo "[nextcloud-app-compat] Skipped: custom apps directory not found: ${apps_dir}"
exit 0
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "[nextcloud-app-compat] Skipped: python3 is required for app compatibility checks."
exit 0
fi
python3 - "${nextcloud_image}" "${apps_dir}" "${list_blocking_apps}" "${repo_root}" "${filter_mode}" <<'PY'
import os
import re
import subprocess
import sys
import xml.etree.ElementTree as ET
image = sys.argv[1].strip()
apps_dir = sys.argv[2]
list_blocking_apps = sys.argv[3] == "1"
repo_root = sys.argv[4]
filter_mode = sys.argv[5]
prefix = "[nextcloud-app-compat]"
def log(message):
if list_blocking_apps:
return
print(f"{prefix} {message}")
def parse_major_from_image(value):
tag = "latest"
last_slash = value.rfind("/")
last_colon = value.rfind(":")
if last_colon > last_slash:
tag = value[last_colon + 1 :]
match = re.match(r"^(\d+)(?:[.-]|$)", tag)
if not match:
return None
return int(match.group(1))
def is_tracked_manifest(manifest):
try:
rel_path = os.path.relpath(manifest, repo_root)
result = subprocess.run(
["git", "-C", repo_root, "ls-files", "--error-unmatch", "--", rel_path],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
return result.returncode == 0
except Exception:
return False
target_major = parse_major_from_image(image)
if target_major is None:
log(f"Skipped: cannot determine target Nextcloud major from image tag: {image}")
sys.exit(0)
manifests = []
for root, _dirs, files in os.walk(apps_dir):
if "info.xml" in files and os.path.basename(root) == "appinfo":
manifest = os.path.join(root, "info.xml")
tracked = is_tracked_manifest(manifest)
if filter_mode == "tracked" and not tracked:
continue
if filter_mode == "untracked" and tracked:
continue
manifests.append(manifest)
if not manifests:
log(f"Skipped: no appinfo/info.xml files found under {apps_dir}")
sys.exit(0)
failures = []
checked = 0
for manifest in sorted(manifests):
try:
tree = ET.parse(manifest)
except ET.ParseError as exc:
failures.append((os.path.basename(os.path.dirname(os.path.dirname(manifest))), f"{manifest}: invalid XML ({exc})"))
continue
root = tree.getroot()
app_id = root.findtext("id") or os.path.basename(os.path.dirname(os.path.dirname(manifest)))
dependency = root.find("./dependencies/nextcloud")
if dependency is None:
failures.append((app_id, f"{app_id}: missing <dependencies><nextcloud .../></dependencies>"))
continue
checked += 1
min_version = dependency.attrib.get("min-version", "").strip()
max_version = dependency.attrib.get("max-version", "").strip()
try:
min_major = int(min_version.split(".", 1)[0]) if min_version else None
max_major = int(max_version.split(".", 1)[0]) if max_version else None
except ValueError:
failures.append((app_id, f"{app_id}: invalid Nextcloud dependency range min={min_version!r} max={max_version!r}"))
continue
if min_major is not None and target_major < min_major:
failures.append((app_id, f"{app_id}: target major {target_major} is below min-version {min_version}"))
if max_major is not None and target_major > max_major:
failures.append((app_id, f"{app_id}: target major {target_major} exceeds max-version {max_version}"))
if failures:
if list_blocking_apps:
for app_id in sorted({app_id for app_id, _message in failures if app_id}):
print(app_id)
sys.exit(1)
log(f"FAILED for target Nextcloud major {target_major} ({image})")
for _app_id, failure in failures:
log(f" - {failure}")
sys.exit(1)
log(f"OK: {checked} custom app manifest(s) declare compatibility with Nextcloud major {target_major}.")
PY