|
@@ -0,0 +1,268 @@
|
|
|
|
|
+#!/usr/bin/env bash
|
|
|
|
|
+# appctl — convenience wrapper for the irdb docker compose stack and CI checks.
|
|
|
|
|
+#
|
|
|
|
|
+# The canonical stack is api + ui + migrate (docker-compose.yml). The
|
|
|
|
|
+# scheduler sidecar (compose.scheduler.yml) is opt-in via --scheduler.
|
|
|
|
|
+#
|
|
|
|
|
+# Per-subproject scope is accepted as an optional positional arg on the
|
|
|
|
|
+# check commands (lint/stan/test/audit/check). With no arg, the command
|
|
|
|
|
+# runs against both api and ui — the same coverage as scripts/ci.sh.
|
|
|
|
|
+#
|
|
|
|
|
+# Lint, stan, test, audit and check all run in ephemeral composer/node
|
|
|
|
|
+# containers (mirroring scripts/ci.sh) so the host needs no PHP or Node
|
|
|
|
|
+# toolchain installed.
|
|
|
|
|
+
|
|
|
|
|
+set -euo pipefail
|
|
|
|
|
+
|
|
|
|
|
+# Resolve repo root regardless of where the user invokes us from. Works
|
|
|
|
|
+# whether called as bin/appctl or via the top-level ./appctl symlink.
|
|
|
|
|
+SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
|
|
|
|
+SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
|
|
|
|
+if [[ "$(basename "$SCRIPT_DIR")" == "bin" ]]; then
|
|
|
|
|
+ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
|
+else
|
|
|
|
|
+ REPO_ROOT="$SCRIPT_DIR"
|
|
|
|
|
+fi
|
|
|
|
|
+cd "$REPO_ROOT"
|
|
|
|
|
+
|
|
|
|
|
+PHP_IMAGE="composer:2"
|
|
|
|
|
+NODE_IMAGE="node:20-alpine"
|
|
|
|
|
+UID_GID="$(id -u):$(id -g)"
|
|
|
|
|
+
|
|
|
|
|
+usage() {
|
|
|
|
|
+ cat <<'EOF'
|
|
|
|
|
+appctl — irdb docker compose / CI wrapper
|
|
|
|
|
+
|
|
|
|
|
+USAGE
|
|
|
|
|
+ appctl <command> [args]
|
|
|
|
|
+
|
|
|
|
|
+STACK (docker compose; canonical stack is api + ui + migrate)
|
|
|
|
|
+ appctl start [--scheduler] [-d] bring the stack up (foreground; -d to detach)
|
|
|
|
|
+ appctl stop [--scheduler] stop and remove containers
|
|
|
|
|
+ appctl build [api|ui|scheduler] build images (no arg = api + ui)
|
|
|
|
|
+ appctl shell [api|ui] bash into a running container (default: api)
|
|
|
|
|
+ appctl logs [service] follow logs (no arg = all services)
|
|
|
|
|
+ appctl ps list stack status (incl. scheduler if running)
|
|
|
|
|
+ appctl migrate run the one-shot migrate container
|
|
|
|
|
+
|
|
|
|
|
+CHECKS (ephemeral containers, no running stack required)
|
|
|
|
|
+ appctl lint [api|ui] composer cs (php-cs-fixer --dry-run)
|
|
|
|
|
+ appctl stan [api|ui] composer stan (phpstan)
|
|
|
|
|
+ appctl test [api|ui] composer test (phpunit)
|
|
|
|
|
+ appctl audit [api|ui] composer audit --no-dev
|
|
|
|
|
+ appctl check [api|ui] lint + stan + audit + test for the target(s)
|
|
|
|
|
+ appctl ci full CI pipeline (delegates to scripts/ci.sh)
|
|
|
|
|
+
|
|
|
|
|
+OTHER
|
|
|
|
|
+ appctl help show this message
|
|
|
|
|
+EOF
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+die() {
|
|
|
|
|
+ printf 'appctl: %s\n' "$*" >&2
|
|
|
|
|
+ exit 2
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# --- compose helpers --------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+# Canonical stack only (api + ui + migrate).
|
|
|
|
|
+compose_main() {
|
|
|
|
|
+ docker compose -f docker-compose.yml "$@"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# Canonical stack plus the scheduler sidecar overlay. Used for start
|
|
|
|
|
+# --scheduler, build scheduler, and read-only commands (logs / ps) that
|
|
|
|
|
+# should still see the sidecar when it's running.
|
|
|
|
|
+compose_full() {
|
|
|
|
|
+ docker compose -f docker-compose.yml -f compose.scheduler.yml "$@"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# Parse `--scheduler` and `-d|--detach` flags out of the args.
|
|
|
|
|
+# Sets globals: SCHED, DETACH, REST (remaining positional args).
|
|
|
|
|
+parse_stack_flags() {
|
|
|
|
|
+ SCHED=0
|
|
|
|
|
+ DETACH=0
|
|
|
|
|
+ REST=()
|
|
|
|
|
+ while (($#)); do
|
|
|
|
|
+ case "$1" in
|
|
|
|
|
+ --scheduler) SCHED=1 ;;
|
|
|
|
|
+ -d|--detach) DETACH=1 ;;
|
|
|
|
|
+ --) shift; REST+=("$@"); break ;;
|
|
|
|
|
+ *) REST+=("$1") ;;
|
|
|
|
|
+ esac
|
|
|
|
|
+ shift
|
|
|
|
|
+ done
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# --- subproject helpers (echo space-separated for `for` loops) --------
|
|
|
|
|
+
|
|
|
|
|
+resolve_targets() {
|
|
|
|
|
+ local arg="${1:-}"
|
|
|
|
|
+ case "$arg" in
|
|
|
|
|
+ "") echo "api ui" ;;
|
|
|
|
|
+ api|ui) echo "$arg" ;;
|
|
|
|
|
+ *) die "unknown subproject: $arg (try: api|ui)" ;;
|
|
|
|
|
+ esac
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# Run a command inside a one-off composer container, mounted at /app.
|
|
|
|
|
+# Mirrors scripts/ci.sh::run_php so behavior stays consistent.
|
|
|
|
|
+run_php() {
|
|
|
|
|
+ local dir="$1"; shift
|
|
|
|
|
+ mkdir -p "$HOME/.composer-cache"
|
|
|
|
|
+ docker run --rm \
|
|
|
|
|
+ -u "$UID_GID" \
|
|
|
|
|
+ -v "$REPO_ROOT/$dir":/app \
|
|
|
|
|
+ -v "$HOME/.composer-cache":/tmp \
|
|
|
|
|
+ -w /app \
|
|
|
|
|
+ -e COMPOSER_HOME=/tmp/composer \
|
|
|
|
|
+ -e COMPOSER_CACHE_DIR=/tmp/composer-cache \
|
|
|
|
|
+ -e XDG_CONFIG_HOME=/tmp \
|
|
|
|
|
+ "$PHP_IMAGE" "$@"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# Bootstrap vendor/ on demand so tests/lint don't fail with a confusing
|
|
|
|
|
+# "vendor/bin/phpunit not found" the first time.
|
|
|
|
|
+ensure_vendor() {
|
|
|
|
|
+ local dir="$1"
|
|
|
|
|
+ [[ -d "$REPO_ROOT/$dir/vendor" ]] && return 0
|
|
|
|
|
+ printf '[appctl] %s/vendor missing — running composer install\n' "$dir"
|
|
|
|
|
+ run_php "$dir" composer install --no-interaction --prefer-dist
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# --- subcommands ------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+cmd_start() {
|
|
|
|
|
+ parse_stack_flags "$@"
|
|
|
|
|
+ local up_args=(up)
|
|
|
|
|
+ [[ "$DETACH" == "1" ]] && up_args+=(-d)
|
|
|
|
|
+ if [[ "$SCHED" == "1" ]]; then
|
|
|
|
|
+ compose_full "${up_args[@]}" "${REST[@]}"
|
|
|
|
|
+ else
|
|
|
|
|
+ compose_main "${up_args[@]}" "${REST[@]}"
|
|
|
|
|
+ fi
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_stop() {
|
|
|
|
|
+ parse_stack_flags "$@"
|
|
|
|
|
+ # `down` operates on the project name and tears down everything
|
|
|
|
|
+ # currently running, regardless of which overlays were used to
|
|
|
|
|
+ # start it — but layering the scheduler overlay keeps things tidy
|
|
|
|
|
+ # when --scheduler was passed.
|
|
|
|
|
+ if [[ "$SCHED" == "1" ]]; then
|
|
|
|
|
+ compose_full down "${REST[@]}"
|
|
|
|
|
+ else
|
|
|
|
|
+ compose_main down "${REST[@]}"
|
|
|
|
|
+ fi
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_build() {
|
|
|
|
|
+ local target="${1:-}"
|
|
|
|
|
+ case "$target" in
|
|
|
|
|
+ "") compose_main build ;;
|
|
|
|
|
+ api|ui|migrate) compose_main build "$target" ;;
|
|
|
|
|
+ scheduler) compose_full build scheduler ;;
|
|
|
|
|
+ *) die "unknown build target: $target (try: api|ui|scheduler|migrate)" ;;
|
|
|
|
|
+ esac
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_shell() {
|
|
|
|
|
+ local service="${1:-api}"
|
|
|
|
|
+ case "$service" in
|
|
|
|
|
+ api|ui) ;;
|
|
|
|
|
+ *) die "unknown shell target: $service (try: api|ui)" ;;
|
|
|
|
|
+ esac
|
|
|
|
|
+ # Both images are alpine-based today; fall back to sh if bash isn't
|
|
|
|
|
+ # present so this keeps working if the base image changes.
|
|
|
|
|
+ compose_main exec "$service" \
|
|
|
|
|
+ sh -c 'command -v bash >/dev/null && exec bash || exec sh'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_logs() {
|
|
|
|
|
+ local service="${1:-}"
|
|
|
|
|
+ if [[ -z "$service" ]]; then
|
|
|
|
|
+ compose_full logs -f
|
|
|
|
|
+ else
|
|
|
|
|
+ compose_full logs -f "$service"
|
|
|
|
|
+ fi
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_ps() {
|
|
|
|
|
+ compose_full ps
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_migrate() {
|
|
|
|
|
+ compose_main run --rm migrate
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_lint() {
|
|
|
|
|
+ local target
|
|
|
|
|
+ for target in $(resolve_targets "${1:-}"); do
|
|
|
|
|
+ ensure_vendor "$target"
|
|
|
|
|
+ printf '\n[appctl] %s: composer cs\n' "$target"
|
|
|
|
|
+ run_php "$target" composer cs
|
|
|
|
|
+ done
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_stan() {
|
|
|
|
|
+ local target
|
|
|
|
|
+ for target in $(resolve_targets "${1:-}"); do
|
|
|
|
|
+ ensure_vendor "$target"
|
|
|
|
|
+ printf '\n[appctl] %s: composer stan\n' "$target"
|
|
|
|
|
+ run_php "$target" composer stan
|
|
|
|
|
+ done
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_test() {
|
|
|
|
|
+ local target
|
|
|
|
|
+ for target in $(resolve_targets "${1:-}"); do
|
|
|
|
|
+ ensure_vendor "$target"
|
|
|
|
|
+ printf '\n[appctl] %s: composer test\n' "$target"
|
|
|
|
|
+ run_php "$target" composer test
|
|
|
|
|
+ done
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_audit() {
|
|
|
|
|
+ local target
|
|
|
|
|
+ for target in $(resolve_targets "${1:-}"); do
|
|
|
|
|
+ ensure_vendor "$target"
|
|
|
|
|
+ printf '\n[appctl] %s: composer audit\n' "$target"
|
|
|
|
|
+ run_php "$target" composer audit --no-dev
|
|
|
|
|
+ done
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_check() {
|
|
|
|
|
+ local target="${1:-}"
|
|
|
|
|
+ cmd_lint "$target"
|
|
|
|
|
+ cmd_stan "$target"
|
|
|
|
|
+ cmd_audit "$target"
|
|
|
|
|
+ cmd_test "$target"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+cmd_ci() {
|
|
|
|
|
+ [[ -x scripts/ci.sh ]] || die "scripts/ci.sh missing or not executable"
|
|
|
|
|
+ exec scripts/ci.sh
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# --- dispatch ---------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+cmd="${1:-help}"
|
|
|
|
|
+shift || true
|
|
|
|
|
+
|
|
|
|
|
+case "$cmd" in
|
|
|
|
|
+ start) cmd_start "$@" ;;
|
|
|
|
|
+ stop) cmd_stop "$@" ;;
|
|
|
|
|
+ build) cmd_build "$@" ;;
|
|
|
|
|
+ shell) cmd_shell "$@" ;;
|
|
|
|
|
+ logs) cmd_logs "$@" ;;
|
|
|
|
|
+ ps) cmd_ps ;;
|
|
|
|
|
+ migrate) cmd_migrate ;;
|
|
|
|
|
+ lint) cmd_lint "$@" ;;
|
|
|
|
|
+ stan) cmd_stan "$@" ;;
|
|
|
|
|
+ test) cmd_test "$@" ;;
|
|
|
|
|
+ audit) cmd_audit "$@" ;;
|
|
|
|
|
+ check) cmd_check "$@" ;;
|
|
|
|
|
+ ci) cmd_ci ;;
|
|
|
|
|
+ help|-h|--help|"") usage ;;
|
|
|
|
|
+ *) die "unknown command: $cmd (try: appctl help)" ;;
|
|
|
|
|
+esac
|