Ctrl K
ComunidadGuías…

run.sh

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
#
# Task runner for a Vaadin and Spring Boot project, built with Maven.
#
# Every task pins JAVA_HOME to the JDK the project is built for, because a bare
# `mvn` picks whatever the machine defaults to — and under a newer JDK than Lombok
# supports it silently generates no getters, so the build dies with bogus
# "cannot find symbol" errors.
#
# Everything specific to one project is in run.conf beside it; nothing in this file
# names a project, so projects share it as it stands. A task this runner does not
# have goes in run.tasks.sh, for the same reason.
#
# Usage: ./run.sh <task> (or `help` to list tasks).

set -euo pipefail

cd "$(dirname "$0")"

readonly PROJECT_CONFIG_FILE="run.conf"

# Tasks a project has that this runner does not: a file of task_<name> functions,
# sourced below the helpers so they can call setup and run_mvn like the tasks here
# do. Keeping them out of this file is what lets a project take a newer runner
# without a merge. It may also define project_usage, printed under the task list.
readonly PROJECT_TASKS_FILE="run.tasks.sh"

if [[ ! -f "${PROJECT_CONFIG_FILE}" ]]; then
    echo "No ${PROJECT_CONFIG_FILE} found" >&2
    exit 1
fi
source "./${PROJECT_CONFIG_FILE}"

# A project has to answer these two; nothing sensible can be assumed for either.
for setting in PROJECT_NAME JAVA_VERSION; do
    if [[ -z "${!setting:-}" ]]; then
        echo "${PROJECT_CONFIG_FILE} sets no ${setting}" >&2
        exit 1
    fi
done
unset setting

# Everything else falls back to the layout a Vaadin project has by default, so a
# configuration only names what it decides. See run.conf for what each one is.
: "${ENV_DIR:=environment/dev}"
: "${CONTAINER_REQUIRED:=true}"
: "${CONTAINER_PREFIX:=${PROJECT_NAME,,}-dev}"
: "${BUNDLE_DIR:=src/main/bundles}"
: "${STYLES_CSS:=src/main/resources/META-INF/resources/styles.css}"
: "${IT_PROFILE:=}"
: "${COMMIT_PROPERTY:=}"

readonly PROJECT_NAME JAVA_VERSION ENV_DIR CONTAINER_REQUIRED CONTAINER_PREFIX
readonly BUNDLE_DIR STYLES_CSS IT_PROFILE COMMIT_PROPERTY

readonly DEV_COMPOSE_FILE="${ENV_DIR}/compose.yaml"
readonly QUADLET_DIR="${ENV_DIR}/quadlet"
readonly QUADLET_INSTALL_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/containers/systemd"

# Filled in by setup, and the only mvn any task runs.
MAVEN=""

# Resolve the configured JDK from SDKMAN; fall back to whatever JAVA_HOME is set.
resolve_java_home() {
    local jdk
    jdk=$(ls -d "${HOME}"/.sdkman/candidates/java/"${JAVA_VERSION}"* 2>/dev/null | head -1 || true)
    if [[ -n "${jdk}" ]]; then
        export JAVA_HOME="${jdk}"
    elif [[ -z "${JAVA_HOME:-}" ]]; then
        echo "No JDK ${JAVA_VERSION} found under ~/.sdkman and JAVA_HOME is unset." >&2
        exit 1
    fi
    echo "Using JAVA_HOME=${JAVA_HOME}"
}

# Which runtime this machine has, or nothing at all. docker first: where both are
# installed, docker is the one whose context and compose plugin this script drives,
# and podman is usually there as its backend.
container_runtime() {
    if command -v docker >/dev/null 2>&1; then
        echo "docker"
    elif command -v podman >/dev/null 2>&1; then
        echo "podman"
    fi
}

require_container_runtime() {
    local runtime
    runtime=$(container_runtime)
    if [[ -z "${runtime}" ]]; then
        echo "No container runtime found. Either docker or podman is required." >&2
        exit 1
    fi
    echo "${runtime}"
}

# Testcontainers finds the daemon through DOCKER_HOST or /var/run/docker.sock and
# looks nowhere else. Neither CLI advertises itself there: docker keeps its endpoint
# in a "context", so on Colima and Rancher Desktop the CLI works while Testcontainers
# reports "Could not find a valid Docker environment", and rootless podman puts its
# socket under /run/user/<uid>. So ask whichever CLI this machine has where its
# socket is, and hand that to Testcontainers.
#
# The override is for Ryuk, the cleanup sidecar, which mounts the socket from inside
# its own container and so needs a path rather than an endpoint. The two runtimes
# disagree on which path: a VM-backed docker sees /var/run/docker.sock however the
# host spells it, while rootless podman runs no VM and needs the real path.
#
# Starting the socket is deliberately left to whoever runs this — the task says what
# to run and stops rather than enabling a system service behind their back.
resolve_container_runtime() {
    if [[ -n "${DOCKER_HOST:-}" ]]; then
        return
    fi
    if [[ -S /var/run/docker.sock ]]; then
        return
    fi

    local runtime endpoint="" override=""
    runtime=$(require_container_runtime)
    case "${runtime}" in
        docker)
            endpoint=$(docker context inspect --format '{{.Endpoints.docker.Host}}' 2>/dev/null || true)
            override="/var/run/docker.sock"
            ;;
        podman)
            local socket
            socket=$(podman info --format '{{.Host.RemoteSocket.Path}}' 2>/dev/null || true)
            if [[ -n "${socket}" ]]; then
                endpoint="unix://${socket}"
                override="${socket}"
            fi
            ;;
    esac

    if [[ -z "${endpoint}" ]]; then
        echo "${runtime} is installed but would not say where its socket is." >&2
        echo "Set DOCKER_HOST by hand, or check that ${runtime} itself works." >&2
        exit 1
    fi

    # podman reports its socket path whether or not anything is listening on it, and
    # a docker context outlives the daemon it points at, so neither answer proves the
    # runtime is up. Say which command starts it instead of failing deep inside a test.
    if [[ "${endpoint}" == unix://* && ! -S "${endpoint#unix://}" ]]; then
        echo "${runtime} reports its socket at ${endpoint#unix://}, but nothing is listening." >&2
        if [[ "${runtime}" == "podman" ]]; then
            echo "Start it with: systemctl --user start podman.socket" >&2
        else
            echo "Start the docker daemon, then retry." >&2
        fi
        exit 1
    fi

    export DOCKER_HOST="${endpoint}"
    export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE="${TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE:-${override}}"
    echo "Using ${runtime} at DOCKER_HOST=${DOCKER_HOST}"
}

# The mvn to run. A bare `mvn` is on the PATH of a login shell only, because SDKMAN
# installs it under ~/.sdkman and the profile is what puts it there — so anything
# starting this script without one, the Claude Code preview pane included, dies with
# "mvn: command not found". Resolve it the way JAVA_HOME is resolved, rather than
# depending on how this script happened to be started.
resolve_maven() {
    if command -v mvn >/dev/null 2>&1; then
        MAVEN="mvn"
        echo "Using MAVEN=${MAVEN} (from the PATH)"
        return
    fi
    local candidate
    candidate=$(ls -d "${HOME}"/.sdkman/candidates/maven/current/bin/mvn \
                      "${HOME}"/.sdkman/candidates/maven/*/bin/mvn 2>/dev/null | head -1 || true)
    if [[ -z "${candidate}" ]]; then
        echo "No mvn on the PATH and none under ~/.sdkman." >&2
        echo "Install Maven, or start this from a shell where SDKMAN is initialised." >&2
        exit 1
    fi
    MAVEN="${candidate}"
    echo "Using MAVEN=${MAVEN}"
}

# What every task needs before it can do anything: the pinned JDK, the mvn that goes
# with it, and the container runtime. Resolved together, in one place, so that a task
# body is the work it is named after and nothing else.
#
# The runtime is not only the test suite's concern, which is why a project that needs
# one resolves it here rather than in the tasks that obviously want it: the application
# itself will not start without the services it talks to, and `package` runs the tests
# on the way through. A machine missing any of it is told so before the work starts
# rather than partway into it.
#
# No exceptions among the tasks either, not even the ones that never reach for a
# container: touching a stylesheet on a machine that cannot run the application
# accomplishes nothing, and failing at the first task is what says so. CONTAINER_REQUIRED
# is the one switch, and it is the project's answer rather than the task's.
setup() {
    resolve_java_home
    resolve_maven
    if [[ "${CONTAINER_REQUIRED}" == "true" ]]; then
        resolve_container_runtime
    fi
}

# Every build carries the deployed commit SHA when the project asks for one, so an
# artefact can be traced back to what produced it. Resolved from the working tree and
# passed to every mvn invocation. A non-repo checkout is then a hard error by design:
# an unidentifiable build should never be produced.
run_mvn() {
    local -a properties=()
    if [[ -n "${COMMIT_PROPERTY}" ]]; then
        local commit
        if ! commit=$(git rev-parse HEAD 2>/dev/null); then
            echo "Cannot resolve the git commit (not a git repository?)." >&2
            echo "${COMMIT_PROPERTY} is mandatory; refusing to build." >&2
            exit 1
        fi
        properties+=("-D${COMMIT_PROPERTY}=${commit}")
    fi
    "${MAVEN}" "$@" "${properties[@]}"
}

# Bump styles.css mtime. styles.css is only @import statements; editing an
# imported partial (colors.css, grid.css, ...) leaves styles.css unchanged, so
# the browser serves a 304 and keeps the stale partial. Touching the entry file
# busts that cache.
touch_styles() {
    touch "${STYLES_CSS}"
    echo "Touched ${STYLES_CSS}"
}

# Remove the cached frontend bundles. Vaadin's dev server reuses an existing
# bundle and logs "a development mode bundle build is not needed", so a changed
# @CssImport(themeFor=...) is ignored until both bundles are gone. Deleting only
# dev.bundle is not enough — a leftover prod.bundle is still treated as valid.
clear_bundles() {
    rm -rf "${BUNDLE_DIR}/dev.bundle" "${BUNDLE_DIR}/prod.bundle" target/dev-bundle
    echo "Cleared cached frontend bundles"
}

task_compile() {
    setup
    run_mvn -q compile
    echo "Compiled. spring-boot-devtools will hot-restart a running app."
}

# Force a clean frontend rebuild after a @CssImport / @JsModule change: drop the
# cached bundles, touch styles.css, then compile so devtools rebuilds the bundle.
task_bundle() {
    setup
    clear_bundles
    touch_styles
    run_mvn -q compile
    echo "Bundle cleared and recompiled. Reload the browser to pick up the new bundle."
}

task_styles() {
    setup
    touch_styles
    run_mvn -q process-resources
    echo "Stylesheets processed. Reload the browser."
}

# Docker renamed compose from a script to a subcommand and both are still in the
# wild, so find whichever this machine has rather than guessing. Only the docker
# shapes: podman drives the stack through quadlets instead.
compose() {
    if docker compose version >/dev/null 2>&1; then
        docker compose -f "${DEV_COMPOSE_FILE}" "$@"
    elif command -v docker-compose >/dev/null 2>&1; then
        docker-compose -f "${DEV_COMPOSE_FILE}" "$@"
    else
        echo "No Compose found. Install the Docker Compose plugin (docker compose)" >&2
        echo "or the standalone docker-compose, then retry." >&2
        exit 1
    fi
}

# The units of the development stack, derived from what is in the quadlet directory
# so that adding a service there needs no change here — the same property the compose
# file has.
quadlet_services() {
    local unit
    for unit in "${QUADLET_DIR}"/*.container.in; do
        echo "$(basename "${unit}" .container.in).service"
    done
}

# Copy the units into the directory systemd's generator reads, substituting the one
# thing a quadlet cannot state for itself: where this checkout lives, which the two
# files mounted into Keycloak need as an absolute path.
install_quadlets() {
    mkdir -p "${QUADLET_INSTALL_DIR}"
    local unit
    for unit in "${QUADLET_DIR}"/*.in; do
        sed "s|@ENV_DIR@|${PWD}/${ENV_DIR}|g" \
            "${unit}" > "${QUADLET_INSTALL_DIR}/$(basename "${unit}" .in)"
    done
    systemctl --user daemon-reload
    echo "Installed quadlet units into ${QUADLET_INSTALL_DIR}"
}

# podman drives the stack through quadlets rather than compose: systemd starts the
# units, orders them, and — because every container declares Notify=healthy — reports
# one as started only once its healthcheck passes. That is what `up --wait` and
# `depends_on: service_healthy` bought under compose, so nothing is lost by not
# having a compose file in the loop.
quadlet_env() {
    local action="$1"
    local -a services
    mapfile -t services < <(quadlet_services)

    case "${action}" in
        up)
            install_quadlets
            systemctl --user start "${services[@]}"
            echo "Development stack is up. ${PROJECT_NAME} needs no configuration to reach it."
            echo "It is started, not enabled: add 'systemctl --user enable' yourself to"
            echo "get it back after a reboot."
            ;;
        down)
            systemctl --user stop "${services[@]}"
            echo "Development stack stopped; its data is kept."
            ;;
        logs)
            local -a follow=()
            local service
            for service in "${services[@]}"; do
                follow+=(--unit "${service}")
            done
            journalctl --user --follow "${follow[@]}"
            ;;
        reset)
            systemctl --user stop "${services[@]}"
            # Every volume of the stack rather than a named one, for the same reason
            # the services are read from a directory: this should not need editing
            # when the stack gains storage.
            podman volume ls --filter name="${CONTAINER_PREFIX}" --quiet \
                | xargs --no-run-if-empty podman volume rm --force >/dev/null
            echo "Development stack stopped and its data thrown away."
            ;;
    esac
}

compose_env() {
    local action="$1"
    case "${action}" in
        # --wait rather than plain -d: every service declares a healthcheck, and the
        # app starting before them is what turns a slow container into a confusing
        # connection or discovery error at startup.
        up)    compose up -d --wait && echo "Development stack is up. ${PROJECT_NAME} needs no configuration to reach it." ;;
        down)  compose stop && echo "Development stack stopped; its data is kept." ;;
        logs)  compose logs -f ;;
        reset) compose down -v && echo "Development stack stopped and its data thrown away." ;;
    esac
}

# The whole development stack in one task, because an application that needs a
# database, a browser and an identity provider needs all of them.
#
# Which containers that means is never this function's decision — compose reads the
# stack's compose.yaml, quadlets come from its quadlet directory — so adding a
# service needs no change here.
task_env() {
    setup

    if [[ "${CONTAINER_REQUIRED}" != "true" ]]; then
        echo "CONTAINER_REQUIRED is ${CONTAINER_REQUIRED}: this project is configured to need" >&2
        echo "no containers, so there is no development stack for this task to bring up." >&2
        exit 1
    fi

    local action="${1:-up}"
    case "${action}" in
        up|down|logs|reset) ;;
        *)
            echo "Unknown env action: ${action} (expected up, down, logs or reset)" >&2
            exit 1
            ;;
    esac

    local runtime
    runtime=$(require_container_runtime)
    if [[ "${runtime}" == "podman" ]]; then
        quadlet_env "${action}"
    else
        compose_env "${action}"
    fi
}

# Warm the local Maven repository ahead of a build, so a later task needs no
# network. dependency:resolve would walk the dependency tree alone and leave the
# build plugins out; go-offline takes both.
#
# It cannot be exhaustive: surefire picks its provider (surefire-junit-platform)
# off the test classpath as it runs, and no resolution goal sees that in advance.
# The first `test` downloads it. That is a slow first run, not a broken one.
task_deps() {
    setup
    run_mvn --batch-mode --no-transfer-progress dependency:go-offline
}

task_test() {
    setup
    # The pom's JaCoCo gate runs with these, and fails the task under its threshold.
    run_mvn test "$@"
}

task_run() {
    setup
    run_mvn spring-boot:run
}

# Same as `run`, but named for the Claude Code preview pane, which launches the
# app through this task (see .claude/launch.json) so the preview goes through
# run.sh — the pinned JDK and the bundle gotchas — like every other task, instead
# of invoking mvn directly.
task_preview() {
    task_run
}

# Unit tests plus the Playwright integration tests, against a production build.
#
# The IT profile is what puts the app in production mode; without it the ITs open a
# dev-mode page and find an empty screen. The bundles are cleared first because a
# dev.bundle left behind by `./run.sh run` makes the frontend build decide "a
# production mode bundle build is not needed" and the app then serves a bundle
# that fails to boot — the same trap `clean` exists for, and `mvn clean` alone
# does not clear it since the bundles live under src/.
task_verify() {
    setup
    clear_bundles
    local -a profile=()
    if [[ -n "${IT_PROFILE}" ]]; then
        profile+=("-P${IT_PROFILE}")
    fi
    run_mvn clean verify "${profile[@]}" "$@"
}

# Full production build.
task_package() {
    setup
    run_mvn clean package "$@"
}

task_clean() {
    setup
    clear_bundles
    "${MAVEN}" clean
}

# Describes only what this runner does, never what a project's build is configured to
# do with it: shared text that claims a coverage gate or a browser test is wrong for
# the first project that has neither, and a listed task that then refuses is worse
# than one that was never offered.
usage() {
    echo "Usage: ./run.sh <task>"
    echo
    echo "Tasks:"
    if [[ "${CONTAINER_REQUIRED}" == "true" ]]; then
        cat <<'EOF'
  env [act]  The whole development stack — quadlets under podman, compose under
             docker: up (default), down, logs, reset (throws the data away)
EOF
    fi
    cat <<'EOF'
  deps       Pre-download dependencies and build plugins, so a later task
             needs no network
  compile    Compile sources (triggers a devtools hot-restart of a running app)
  bundle     Clear cached frontend bundles + touch styles.css + recompile
             (use after changing a @CssImport themeFor / @JsModule)
  styles     Touch styles.css so the browser reloads @imported CSS partials
  test       Run the unit tests (mvn test)
  verify     Unit tests + integration tests (mvn clean verify)
  run        Start the app with spring-boot:run (dev mode)
  preview    Alias of run, launched by the Claude Code preview pane
  package    Full production build (mvn clean package)
  clean      mvn clean + remove cached bundles
  help       Show this message
EOF
    if declare -F project_usage >/dev/null; then
        project_usage
    fi
}

main() {
    local task="${1:-help}"
    case "${task}" in
        compile) task_compile ;;
        bundle)  task_bundle ;;
        styles)  task_styles ;;
        env)     task_env "${2:-up}" ;;
        deps)    task_deps ;;
        test)    task_test "${@:2}" ;;
        verify)  task_verify "${@:2}" ;;
        run)     task_run ;;
        preview) task_preview ;;
        package) task_package "${@:2}" ;;
        clean)   task_clean ;;
        help|-h|--help) usage ;;
        *)
            # Anything the project's own task file defines, dispatched by name. Only
            # a declared function matches, so a mistyped task is still a mistyped
            # task rather than a silent no-op.
            if declare -F "task_${task}" >/dev/null; then
                "task_${task}" "${@:2}"
            else
                echo "Unknown task: ${task}" >&2
                echo >&2
                usage >&2
                exit 1
            fi
            ;;
    esac
}

if [[ -n "${PROJECT_TASKS_FILE}" && -f "${PROJECT_TASKS_FILE}" ]]; then
    source "./${PROJECT_TASKS_FILE}"
fi

main "$@"