From 2c8ede44d47f2cbaadc94b77df86ef8d947f8204 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:27:09 +0000 Subject: [PATCH 1/7] feat(ncdd): add unified development runtime CLI Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/dev-worker/Dockerfile | 9 ++ .ncdd.yml.example | 15 ++ bin/ncdd | 293 ++++++++++++++++++++++++++++++++++ docker-compose.dev.yml | 84 ++++++++++ 4 files changed, 401 insertions(+) create mode 100644 .docker/dev-worker/Dockerfile create mode 100644 .ncdd.yml.example create mode 100644 bin/ncdd create mode 100644 docker-compose.dev.yml diff --git a/.docker/dev-worker/Dockerfile b/.docker/dev-worker/Dockerfile new file mode 100644 index 0000000..1356a91 --- /dev/null +++ b/.docker/dev-worker/Dockerfile @@ -0,0 +1,9 @@ +ARG NCDD_APP_IMAGE=ghcr.io/librecodecoop/nextcloud-docker-app:35 +FROM composer:2 AS composer +FROM ${NCDD_APP_IMAGE} + +RUN apt-get update \ + && apt-get install -y --no-install-recommends git unzip \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=composer /usr/bin/composer /usr/local/bin/composer diff --git a/.ncdd.yml.example b/.ncdd.yml.example new file mode 100644 index 0000000..2e5adf9 --- /dev/null +++ b/.ncdd.yml.example @@ -0,0 +1,15 @@ +nextcloud: + default: stable35 + +app: + id: myapp + source: . + path: /var/www/html/apps-extra/myapp + runtime_user: www-data + +services: + app: app + worker: dev-worker + +http: + port: 8080 diff --git a/bin/ncdd b/bin/ncdd new file mode 100644 index 0000000..341099d --- /dev/null +++ b/bin/ncdd @@ -0,0 +1,293 @@ +#!/usr/bin/env bash +set -euo pipefail + +NCDD_VERSION="0.1.0" +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +NCDD_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd) +PROJECT_ROOT=${NCDD_PROJECT_ROOT:-$PWD} +CONFIG_FILE=${NCDD_CONFIG_FILE:-} +NEXTCLOUD_INPUT=${NCDD_NEXTCLOUD:-} +APP_SOURCE=${NCDD_APP_SOURCE:-} +APP_ID=${NCDD_APP_ID:-} + +usage() { + cat <<'USAGE' +Usage: ncdd [global options] [options] + +Global options: + --project-root PATH Consumer repository root (default: current directory) + --config PATH NCDD config file (default: /.ncdd.yml) + --nextcloud REF Nextcloud ref/major (35, stable35, main) + --source PATH App source checkout to mount + --app-id ID Nextcloud app id + --version Show NCDD version + +Commands: + up Start the disposable development stack + down Stop and remove the stack + status Show compose service status + env Show the resolved NCDD environment + doctor [--json] Validate Docker, stack and Nextcloud readiness + shell Open a shell in the development worker + exec [--as USER] -- COMMAND... + Execute a command in the development worker + help [ai] Show help; `help ai` prints the agent contract +USAGE +} + +agent_help() { + cat <<'EOF_AGENT' +NCDD agent contract +- Use `ncdd up`, `ncdd status`, `ncdd doctor --json`, `ncdd exec`, `ncdd test`, and `ncdd down`. +- Do not call docker compose directly unless debugging NCDD itself. +- Do not install PHP, Composer, Node, or Nextcloud on the host. +- Keep private Git authentication on the host/CI checkout; mount the checkout with `--source`. +- Use `--as runtime` for commands that must run as the Nextcloud runtime user. +EOF_AGENT +} + +fail() { + printf 'ncdd: %s\n' "$*" >&2 + exit 1 +} + +yaml_get() { + local key=$1 file=$2 + [ -f "$file" ] || return 1 + awk -v wanted="$key" ' + function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); return s } + function unquote(s) { + s=trim(s) + if ((substr(s,1,1)=="\"" && substr(s,length(s),1)=="\"") || (substr(s,1,1)=="\047" && substr(s,length(s),1)=="\047")) { + return substr(s,2,length(s)-2) + } + return s + } + /^[[:space:]]*#/ || /^[[:space:]]*$/ { next } + { + match($0, /^[ ]*/); spaces=RLENGTH + level=int(spaces/2)+1 + line=substr($0, spaces+1) + pos=index(line, ":") + if (!pos) next + k=trim(substr(line,1,pos-1)) + v=trim(substr(line,pos+1)) + keys[level]=k + for (i=level+1;i<=12;i++) delete keys[i] + path=keys[1] + for (i=2;i<=level;i++) path=path "." keys[i] + if (path==wanted && v!="") { print unquote(v); exit } + } + ' "$file" +} + +abs_path() { + local path=$1 base=$2 + if [[ "$path" = /* ]]; then + printf '%s\n' "$path" + else + (cd "$base" && cd "$path" && pwd) + fi +} + +normalize_nextcloud() { + local input=${1:-} + case "$input" in + '') printf 'stable35\n' ;; + stable[0-9]*) printf '%s\n' "$input" ;; + [0-9]*) printf 'stable%s\n' "$input" ;; + main|master) printf '%s\n' "$input" ;; + *) fail "unsupported Nextcloud ref: $input" ;; + esac +} + +nextcloud_major() { + case "$1" in + stable[0-9]*) printf '%s\n' "${1#stable}" ;; + [0-9]*) printf '%s\n' "$1" ;; + main|master) printf 'main\n' ;; + *) fail "cannot determine Nextcloud major from $1" ;; + esac +} + +sanitize_project_name() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_-]+/-/g; s/^-+//; s/-+$//' +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --project-root) PROJECT_ROOT=${2:?}; shift 2 ;; + --config) CONFIG_FILE=${2:?}; shift 2 ;; + --nextcloud) NEXTCLOUD_INPUT=${2:?}; shift 2 ;; + --source) APP_SOURCE=${2:?}; shift 2 ;; + --app-id) APP_ID=${2:?}; shift 2 ;; + --version) printf 'ncdd %s\n' "$NCDD_VERSION"; exit 0 ;; + -h|--help) usage; exit 0 ;; + --) shift; break ;; + -*) break ;; + *) break ;; + esac +done + +COMMAND=${1:-help} +[ "$#" -gt 0 ] && shift || true + +PROJECT_ROOT=$(abs_path "$PROJECT_ROOT" "$PWD") +if [ -z "$CONFIG_FILE" ]; then + CONFIG_FILE="$PROJECT_ROOT/.ncdd.yml" +elif [[ "$CONFIG_FILE" != /* ]]; then + CONFIG_FILE="$PROJECT_ROOT/$CONFIG_FILE" +fi + +cfg() { yaml_get "$1" "$CONFIG_FILE" 2>/dev/null || true; } + +[ -n "$NEXTCLOUD_INPUT" ] || NEXTCLOUD_INPUT=$(cfg nextcloud.default) +NEXTCLOUD_REF=$(normalize_nextcloud "$NEXTCLOUD_INPUT") +NEXTCLOUD_MAJOR=$(nextcloud_major "$NEXTCLOUD_REF") + +[ -n "$APP_ID" ] || APP_ID=$(cfg app.id) +[ -n "$APP_ID" ] || APP_ID=$(basename "$PROJECT_ROOT") +[ -n "$APP_SOURCE" ] || APP_SOURCE=$(cfg app.source) +[ -n "$APP_SOURCE" ] || APP_SOURCE='.' +APP_SOURCE=$(abs_path "$APP_SOURCE" "$PROJECT_ROOT") +APP_PATH=$(cfg app.path) +[ -n "$APP_PATH" ] || APP_PATH="/var/www/html/apps-extra/$APP_ID" +RUNTIME_USER=$(cfg app.runtime_user) +[ -n "$RUNTIME_USER" ] || RUNTIME_USER=www-data +APP_SERVICE=$(cfg services.app) +[ -n "$APP_SERVICE" ] || APP_SERVICE=app +WORKER_SERVICE=$(cfg services.worker) +[ -n "$WORKER_SERVICE" ] || WORKER_SERVICE=dev-worker +COMPOSE_FILE=$(cfg compose.file) +[ -n "$COMPOSE_FILE" ] || COMPOSE_FILE="$NCDD_ROOT/docker-compose.dev.yml" +if [[ "$COMPOSE_FILE" != /* ]]; then COMPOSE_FILE="$PROJECT_ROOT/$COMPOSE_FILE"; fi +HTTP_PORT=$(cfg http.port) +[ -n "$HTTP_PORT" ] || HTTP_PORT=${NCDD_HTTP_PORT:-8080} +PROJECT_NAME=$(sanitize_project_name "ncdd-${APP_ID}-${NEXTCLOUD_MAJOR}") +APP_IMAGE=${NCDD_APP_IMAGE:-ghcr.io/librecodecoop/nextcloud-docker-app:${NEXTCLOUD_MAJOR}} + +export NCDD_PROJECT_ROOT="$PROJECT_ROOT" +export NCDD_APP_SOURCE="$APP_SOURCE" +export NCDD_APP_ID="$APP_ID" +export NCDD_APP_PATH="$APP_PATH" +export NCDD_RUNTIME_USER="$RUNTIME_USER" +export NCDD_NEXTCLOUD_REF="$NEXTCLOUD_REF" +export NCDD_NEXTCLOUD_MAJOR="$NEXTCLOUD_MAJOR" +export NCDD_HTTP_PORT="$HTTP_PORT" +export NCDD_APP_IMAGE="$APP_IMAGE" + +compose() { + docker compose --project-name "$PROJECT_NAME" -f "$COMPOSE_FILE" "$@" +} + +resolve_user() { + case "$1" in + runtime) printf '%s\n' "$RUNTIME_USER" ;; + developer|root) printf 'root\n' ;; + *) printf '%s\n' "$1" ;; + esac +} + +print_env() { + cat </dev/null 2>&1 && docker_ok=true + if $docker_ok && docker compose version >/dev/null 2>&1; then compose_ok=true; fi + if $compose_ok && compose ps --status running --services 2>/dev/null | grep -qx "$APP_SERVICE"; then app_running=true; fi + if $app_running && compose exec -T --user "$RUNTIME_USER" "$APP_SERVICE" php occ status --output=json >/dev/null 2>&1; then nc_ready=true; fi + + local ready=false + if $docker_ok && $compose_ok && $app_running && $nc_ready; then ready=true; fi + + if $json; then + printf '{"ready":%s,"docker":%s,"compose":%s,"nextcloud":{"ref":"%s","major":"%s","ready":%s},"app":{"id":"%s","source":"%s","path":"%s","running":%s}}\n' \ + "$ready" "$docker_ok" "$compose_ok" "$NEXTCLOUD_REF" "$NEXTCLOUD_MAJOR" "$nc_ready" \ + "$APP_ID" "$APP_SOURCE" "$APP_PATH" "$app_running" + else + printf '%-20s %s\n' 'Docker' "$docker_ok" + printf '%-20s %s\n' 'Docker Compose' "$compose_ok" + printf '%-20s %s\n' 'Nextcloud ref' "$NEXTCLOUD_REF" + printf '%-20s %s\n' 'Nextcloud ready' "$nc_ready" + printf '%-20s %s\n' 'App' "$APP_ID" + printf '%-20s %s\n' 'App source' "$APP_SOURCE" + printf '%-20s %s\n' 'App running' "$app_running" + fi + $ready +} + +case "$COMMAND" in + help) + if [ "${1:-}" = ai ]; then agent_help; else usage; fi + ;; + env) + print_env + ;; + up) + while [ "$#" -gt 0 ]; do + case "$1" in + --nextcloud) + NEXTCLOUD_REF=$(normalize_nextcloud "${2:?}") + NEXTCLOUD_MAJOR=$(nextcloud_major "$NEXTCLOUD_REF") + PROJECT_NAME=$(sanitize_project_name "ncdd-${APP_ID}-${NEXTCLOUD_MAJOR}") + APP_IMAGE=${NCDD_APP_IMAGE:-ghcr.io/librecodecoop/nextcloud-docker-app:${NEXTCLOUD_MAJOR}} + export NCDD_NEXTCLOUD_REF NCDD_NEXTCLOUD_MAJOR NCDD_APP_IMAGE + shift 2 + ;; + --source) + APP_SOURCE=$(abs_path "${2:?}" "$PROJECT_ROOT") + export NCDD_APP_SOURCE="$APP_SOURCE" + shift 2 + ;; + *) fail "unknown up option: $1" ;; + esac + done + compose up -d --remove-orphans + ;; + down) + compose down --remove-orphans "$@" + ;; + status) + compose ps "$@" + ;; + doctor) + doctor "$@" + ;; + shell) + user=root + if [ "${1:-}" = "--as" ]; then user=$(resolve_user "${2:?}"); shift 2; fi + [ "$#" -eq 0 ] || fail "unknown shell arguments: $*" + compose exec --user "$user" "$WORKER_SERVICE" bash + ;; + exec) + user=root + if [ "${1:-}" = "--as" ]; then user=$(resolve_user "${2:?}"); shift 2; fi + if [ "${1:-}" = "--" ]; then shift; fi + [ "$#" -gt 0 ] || fail "exec requires a command" + tty=() + if [ ! -t 0 ] || [ ! -t 1 ]; then tty=(-T); fi + compose exec "${tty[@]}" --user "$user" "$WORKER_SERVICE" "$@" + ;; + *) + fail "unknown command: $COMMAND" + ;; +esac diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..4ca2353 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,84 @@ +name: ${NCDD_PROJECT_NAME:-ncdd} + +services: + db: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${NCDD_POSTGRES_DB:-nextcloud} + POSTGRES_USER: ${NCDD_POSTGRES_USER:-nextcloud} + POSTGRES_PASSWORD: ${NCDD_POSTGRES_PASSWORD:-nextcloud} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${NCDD_POSTGRES_USER:-nextcloud} -d ${NCDD_POSTGRES_DB:-nextcloud}"] + interval: 2s + timeout: 3s + retries: 30 + volumes: + - db:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 2s + timeout: 3s + retries: 30 + + app: + image: ${NCDD_APP_IMAGE:-ghcr.io/librecodecoop/nextcloud-docker-app:35} + environment: + POSTGRES_DB: ${NCDD_POSTGRES_DB:-nextcloud} + POSTGRES_USER: ${NCDD_POSTGRES_USER:-nextcloud} + POSTGRES_PASSWORD: ${NCDD_POSTGRES_PASSWORD:-nextcloud} + POSTGRES_HOST: db + REDIS_HOST: redis + NEXTCLOUD_ADMIN_USER: ${NCDD_ADMIN_USER:-admin} + NEXTCLOUD_ADMIN_PASSWORD: ${NCDD_ADMIN_PASSWORD:-admin} + NEXTCLOUD_TRUSTED_DOMAINS: ${NCDD_TRUSTED_DOMAINS:-localhost 127.0.0.1 web} + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - nextcloud:/var/www/html + - ${NCDD_APP_SOURCE:?set NCDD_APP_SOURCE}:${NCDD_APP_PATH:?set NCDD_APP_PATH} + + web: + build: + context: .docker/web + ports: + - "${NCDD_HTTP_PORT:-8080}:80" + depends_on: + - app + volumes: + - nextcloud:/var/www/html:ro + - ${NCDD_APP_SOURCE:?set NCDD_APP_SOURCE}:${NCDD_APP_PATH:?set NCDD_APP_PATH}:ro + + dev-worker: + build: + context: . + dockerfile: .docker/dev-worker/Dockerfile + args: + NCDD_APP_IMAGE: ${NCDD_APP_IMAGE:-ghcr.io/librecodecoop/nextcloud-docker-app:35} + entrypoint: ["/bin/sh", "-c", "trap : TERM INT; sleep infinity & wait"] + working_dir: ${NCDD_APP_PATH:?set NCDD_APP_PATH} + environment: + BEHAT_ROOT_DIR: /var/www/html + BEHAT_RUN_AS: ${NCDD_RUNTIME_USER:-www-data} + BEHAT_VERBOSE: "1" + depends_on: + - app + volumes: + - nextcloud:/var/www/html + - ${NCDD_APP_SOURCE:?set NCDD_APP_SOURCE}:${NCDD_APP_PATH:?set NCDD_APP_PATH} + + node-worker: + image: node:22-bookworm + entrypoint: ["/bin/sh", "-c", "trap : TERM INT; sleep infinity & wait"] + working_dir: /workspace/app + volumes: + - ${NCDD_APP_SOURCE:?set NCDD_APP_SOURCE}:/workspace/app + +volumes: + db: + nextcloud: From eb314b2be618ea58e523268c417f9e463b9e6a27 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:27:46 +0000 Subject: [PATCH 2/7] test(ncdd): document and cover the runtime contract Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- AGENTS.md | 11 ++++++++ Makefile | 7 +++-- README.md | 15 ++++++++++- docs/ncdd.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test-ncdd.sh | 58 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 AGENTS.md create mode 100644 docs/ncdd.md create mode 100644 tests/test-ncdd.sh diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..abdf031 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,11 @@ +# NCDD agent contract + +NCDD is the supported interface for development and test automation in this repository. + +- Use `ncdd up`, `ncdd status`, `ncdd doctor --json`, `ncdd exec`, `ncdd shell`, `ncdd test`, and `ncdd down`. +- Do not call `docker compose` directly unless debugging NCDD itself. +- Do not install PHP, Composer, Node, or Nextcloud on the host. +- Keep private Git authentication in the host or CI checkout. Mount the checkout into NCDD instead of fetching private repositories inside containers. +- Use `--as runtime` for commands that must run as the Nextcloud runtime user. +- Prefer focused `ncdd test` commands before full suites. +- Treat `.ncdd.yml` as the project contract; do not hard-code container names or `/var/www/html` paths in automation. diff --git a/Makefile b/Makefile index 7d27b09..050cbd2 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ COMPOSE ?= docker compose GARAGES3_COMPOSE_FILE ?= docker-compose-garages3.yml -.PHONY: up-garages3 down-garages3 bootstrap-garages3 garage-status-garages3 start-garages3 wait-nextcloud-garages3 setup-garages3 test-hooks test-scan-images scan-images +.PHONY: up-garages3 down-garages3 bootstrap-garages3 garage-status-garages3 start-garages3 wait-nextcloud-garages3 setup-garages3 test-hooks test-scan-images test-ncdd scan-images up-garages3: $(COMPOSE) -f $(GARAGES3_COMPOSE_FILE) up -d garage @@ -32,6 +32,9 @@ test-hooks: test-scan-images: bash tests/test-scan-images.sh +test-ncdd: + bash tests/test-ncdd.sh + scan-images: @set -e; \ version="$$(sed -n 's/^NEXTCLOUD_VERSION=//p' .env.example | head -n 1)"; \ @@ -48,4 +51,4 @@ scan-images: 'app@linux/amd64=nextcloud-app:scan-amd64' \ 'app@linux/arm64=nextcloud-app:scan-arm64' \ 'web@linux/amd64=nextcloud-web:scan-amd64' \ - 'web@linux/arm64=nextcloud-web:scan-arm64' + 'web@linux/arm64=nextcloud-web:scan-arm64' \ No newline at end of file diff --git a/README.md b/README.md index 9574476..2b45c98 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,19 @@ Languages avaliable: [pt-BR](docs/README_ptBR.md) - [Logs](#logs) - [Nextcloud Talk](#talk) +## NCDD development interface + +For local app development, AI agents, CI, Dev Containers, and Codespaces, use the NCDD CLI instead of calling Docker Compose directly. The same interface is intended to run everywhere: + +```bash +/path/to/nextcloud-docker/bin/ncdd up --nextcloud 35 +/path/to/nextcloud-docker/bin/ncdd doctor +/path/to/nextcloud-docker/bin/ncdd shell +/path/to/nextcloud-docker/bin/ncdd down +``` + +See [docs/ncdd.md](docs/ncdd.md) for the project contract, private-checkout workflow, diagnostics, and execution-user aliases. + ## Setup of docker You need to have, on your server, the installed docker. The installation can be done with an official script, following the following steps: @@ -262,4 +275,4 @@ app_1 | 2020-04-28T19:49:38.577733913Z Upgrading nextcloud from 18.0.3.0 .. ## Talk -For setting up Nextcloud Talk with all services, see [here](https://github.com/LibreCodeCoop/nextcloud-docker-talk). +For setting up Nextcloud Talk with all services, see [here](https://github.com/LibreCodeCoop/nextcloud-docker-talk). \ No newline at end of file diff --git a/docs/ncdd.md b/docs/ncdd.md new file mode 100644 index 0000000..e928084 --- /dev/null +++ b/docs/ncdd.md @@ -0,0 +1,64 @@ +# NCDD development interface + +NCDD provides one interface for local development, AI agents, CI, Dev Containers, and Codespaces. Docker Compose is an implementation detail behind the `ncdd` command. + +## Quick start + +From an app checkout, copy `.ncdd.yml.example` to `.ncdd.yml` and adjust the app id if necessary. Then run: + +```bash +/path/to/nextcloud-docker/bin/ncdd up --nextcloud 35 +/path/to/nextcloud-docker/bin/ncdd doctor +/path/to/nextcloud-docker/bin/ncdd shell +/path/to/nextcloud-docker/bin/ncdd down +``` + +`35` and `stable35` resolve to the same Nextcloud ref. The source checkout is mounted into the environment; private Git authentication never needs to be copied into a container. + +## Project contract + +`.ncdd.yml` supports the following scalar settings: + +```yaml +nextcloud: + default: stable35 +app: + id: libresign + source: . + path: /var/www/html/apps-extra/libresign + runtime_user: www-data +services: + app: app + worker: dev-worker +http: + port: 8080 +``` + +Environment variables and command-line flags can override the contract. `NCDD_APP_IMAGE` can select a prebuilt Nextcloud development image explicitly. + +## Diagnostics + +`ncdd env` prints the fully resolved environment. `ncdd doctor` checks Docker, Compose, the app service, and `occ status`. Agents and CI should prefer `ncdd doctor --json`. + +## Execution users + +`ncdd exec` defaults to the development worker. Use aliases instead of implementation-specific usernames: + +```bash +ncdd exec -- composer dump-autoload +ncdd exec --as runtime -- php occ status +ncdd exec --as root -- id +``` + +`runtime` resolves to `app.runtime_user` (normally `www-data`). + +## Private branches and forks + +Check out private code on the host or in the CI workspace and mount it: + +```bash +gh repo clone OWNER/PRIVATE-FORK app +ncdd --project-root app --source app up --nextcloud stable35 +``` + +Do not run `git fetch` for private repositories from inside the runtime containers. diff --git a/tests/test-ncdd.sh b/tests/test-ncdd.sh new file mode 100644 index 0000000..cd3b7c8 --- /dev/null +++ b/tests/test-ncdd.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +mkdir -p "$tmp/project" "$tmp/bin" + +cat > "$tmp/project/.ncdd.yml" <<'YAML' +nextcloud: + default: 35 +app: + id: demo + source: . + path: /var/www/html/apps-extra/demo + runtime_user: www-data +services: + app: app + worker: dev-worker +http: + port: 8888 +YAML + +cat > "$tmp/bin/docker" <<'MOCK' +#!/usr/bin/env bash +set -euo pipefail +printf '%q ' "$@" >> "${NCDD_TEST_LOG:?}" +printf '\n' >> "${NCDD_TEST_LOG:?}" +if [ "${1:-}" = compose ] && [ "${2:-}" = version ]; then exit 0; fi +case " $* " in + *" ps --status running --services "*) printf 'app\ndev-worker\n'; exit 0 ;; + *" php occ status --output=json "*) printf '{"installed":true}\n'; exit 0 ;; +esac +exit 0 +MOCK +chmod +x "$tmp/bin/docker" + +export PATH="$tmp/bin:$PATH" +export NCDD_TEST_LOG="$tmp/docker.log" + +out=$(cd "$tmp/project" && "$repo_root/bin/ncdd" env) +grep -q '^NCDD_NEXTCLOUD_REF=stable35$' <<<"$out" +grep -q '^NCDD_NEXTCLOUD_MAJOR=35$' <<<"$out" +grep -q '^NCDD_APP_ID=demo$' <<<"$out" +grep -q '^NCDD_HTTP_PORT=8888$' <<<"$out" + +json=$(cd "$tmp/project" && "$repo_root/bin/ncdd" doctor --json) +grep -q '"ready":true' <<<"$json" +grep -q '"ref":"stable35"' <<<"$json" + +(cd "$tmp/project" && "$repo_root/bin/ncdd" exec --as runtime -- php -v) +grep -q -- '--user www-data dev-worker php -v' "$tmp/docker.log" + +help=$("$repo_root/bin/ncdd" help ai) +grep -q 'Do not call docker compose directly' <<<"$help" + +bash -n "$repo_root/bin/ncdd" +echo 'ncdd CLI tests passed' From d320d3a8229e45de8505d08cfdc33ab7f0a26fc1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:53:28 +0000 Subject: [PATCH 3/7] refactor(ncdd): reuse existing development stack Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/dev-worker/Dockerfile | 9 - .github/workflows/ncdd-cli.yml | 46 +++++ .ncdd.yml.example | 15 -- AGENTS.md | 12 +- Makefile | 4 +- README.md | 16 +- bin/ncdd | 337 +++++++++++---------------------- docker-compose.dev.yml | 84 -------- docs/ncdd.md | 85 ++++----- tests/ncdd.bats | 81 ++++++++ tests/test-ncdd.sh | 58 ------ 11 files changed, 295 insertions(+), 452 deletions(-) delete mode 100644 .docker/dev-worker/Dockerfile create mode 100644 .github/workflows/ncdd-cli.yml delete mode 100644 .ncdd.yml.example delete mode 100644 docker-compose.dev.yml create mode 100644 tests/ncdd.bats delete mode 100644 tests/test-ncdd.sh diff --git a/.docker/dev-worker/Dockerfile b/.docker/dev-worker/Dockerfile deleted file mode 100644 index 1356a91..0000000 --- a/.docker/dev-worker/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -ARG NCDD_APP_IMAGE=ghcr.io/librecodecoop/nextcloud-docker-app:35 -FROM composer:2 AS composer -FROM ${NCDD_APP_IMAGE} - -RUN apt-get update \ - && apt-get install -y --no-install-recommends git unzip \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=composer /usr/bin/composer /usr/local/bin/composer diff --git a/.github/workflows/ncdd-cli.yml b/.github/workflows/ncdd-cli.yml new file mode 100644 index 0000000..a13ee15 --- /dev/null +++ b/.github/workflows/ncdd-cli.yml @@ -0,0 +1,46 @@ +name: NCDD CLI + +on: + pull_request: + paths: + - 'bin/ncdd' + - 'tests/ncdd.bats' + - 'docs/ncdd.md' + - 'AGENTS.md' + - 'Makefile' + - '.github/workflows/ncdd-cli.yml' + push: + branches: + - main + paths: + - 'bin/ncdd' + - 'tests/ncdd.bats' + - 'docs/ncdd.md' + - 'AGENTS.md' + - 'Makefile' + - '.github/workflows/ncdd-cli.yml' + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Setup Bats + uses: bats-core/bats-action@77d6fb60505b4d0d1d73e48bd035b55074bbfb43 # 4.0.0 + with: + bats-version: 1.14.0 + support-install: false + assert-install: false + detik-install: false + file-install: false + + - name: ShellCheck + run: shellcheck bin/ncdd + + - name: Test + run: bats tests/ncdd.bats diff --git a/.ncdd.yml.example b/.ncdd.yml.example deleted file mode 100644 index 2e5adf9..0000000 --- a/.ncdd.yml.example +++ /dev/null @@ -1,15 +0,0 @@ -nextcloud: - default: stable35 - -app: - id: myapp - source: . - path: /var/www/html/apps-extra/myapp - runtime_user: www-data - -services: - app: app - worker: dev-worker - -http: - port: 8080 diff --git a/AGENTS.md b/AGENTS.md index abdf031..491dd61 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,11 +1,7 @@ # NCDD agent contract -NCDD is the supported interface for development and test automation in this repository. +Use `bin/ncdd` for stack operations. Do not reproduce Docker Compose commands in agent instructions unless debugging NCDD itself. -- Use `ncdd up`, `ncdd status`, `ncdd doctor --json`, `ncdd exec`, `ncdd shell`, `ncdd test`, and `ncdd down`. -- Do not call `docker compose` directly unless debugging NCDD itself. -- Do not install PHP, Composer, Node, or Nextcloud on the host. -- Keep private Git authentication in the host or CI checkout. Mount the checkout into NCDD instead of fetching private repositories inside containers. -- Use `--as runtime` for commands that must run as the Nextcloud runtime user. -- Prefer focused `ncdd test` commands before full suites. -- Treat `.ncdd.yml` as the project contract; do not hard-code container names or `/var/www/html` paths in automation. +Keep Git checkout and authentication outside the containers. App worktrees belong under `volumes/nextcloud/apps-extra/`. + +Use `bin/ncdd doctor --json` for machine-readable environment diagnostics. See `docs/ncdd.md` for the command contract. diff --git a/Makefile b/Makefile index 050cbd2..fa9655d 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ test-scan-images: bash tests/test-scan-images.sh test-ncdd: - bash tests/test-ncdd.sh + bats tests/ncdd.bats scan-images: @set -e; \ @@ -51,4 +51,4 @@ scan-images: 'app@linux/amd64=nextcloud-app:scan-amd64' \ 'app@linux/arm64=nextcloud-app:scan-arm64' \ 'web@linux/amd64=nextcloud-web:scan-amd64' \ - 'web@linux/arm64=nextcloud-web:scan-arm64' \ No newline at end of file + 'web@linux/arm64=nextcloud-web:scan-arm64' diff --git a/README.md b/README.md index 2b45c98..9d3ff8c 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,18 @@ Languages avaliable: [pt-BR](docs/README_ptBR.md) - [Logs](#logs) - [Nextcloud Talk](#talk) -## NCDD development interface +## NCDD command interface -For local app development, AI agents, CI, Dev Containers, and Codespaces, use the NCDD CLI instead of calling Docker Compose directly. The same interface is intended to run everywhere: +For development automation, use the repository's `bin/ncdd` wrapper instead of reproducing Docker Compose details in CI, agents, or local instructions. It operates the existing `docker-compose.yml` and `docker-compose-postgres.yml`; it does not define a second stack. ```bash -/path/to/nextcloud-docker/bin/ncdd up --nextcloud 35 -/path/to/nextcloud-docker/bin/ncdd doctor -/path/to/nextcloud-docker/bin/ncdd shell -/path/to/nextcloud-docker/bin/ncdd down +bin/ncdd up +bin/ncdd doctor +bin/ncdd shell +bin/ncdd down ``` -See [docs/ncdd.md](docs/ncdd.md) for the project contract, private-checkout workflow, diagnostics, and execution-user aliases. +See [docs/ncdd.md](docs/ncdd.md) for the command contract and testing policy. ## Setup of docker @@ -275,4 +275,4 @@ app_1 | 2020-04-28T19:49:38.577733913Z Upgrading nextcloud from 18.0.3.0 .. ## Talk -For setting up Nextcloud Talk with all services, see [here](https://github.com/LibreCodeCoop/nextcloud-docker-talk). \ No newline at end of file +For setting up Nextcloud Talk with all services, see [here](https://github.com/LibreCodeCoop/nextcloud-docker-talk). diff --git a/bin/ncdd b/bin/ncdd index 341099d..8021198 100644 --- a/bin/ncdd +++ b/bin/ncdd @@ -1,49 +1,36 @@ #!/usr/bin/env bash set -euo pipefail -NCDD_VERSION="0.1.0" -SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -NCDD_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd) -PROJECT_ROOT=${NCDD_PROJECT_ROOT:-$PWD} -CONFIG_FILE=${NCDD_CONFIG_FILE:-} -NEXTCLOUD_INPUT=${NCDD_NEXTCLOUD:-} -APP_SOURCE=${NCDD_APP_SOURCE:-} -APP_ID=${NCDD_APP_ID:-} +NCDD_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +APP_SERVICE=${NCDD_APP_SERVICE:-app} +RUNTIME_USER=${NCDD_RUNTIME_USER:-www-data} usage() { - cat <<'USAGE' -Usage: ncdd [global options] [options] - -Global options: - --project-root PATH Consumer repository root (default: current directory) - --config PATH NCDD config file (default: /.ncdd.yml) - --nextcloud REF Nextcloud ref/major (35, stable35, main) - --source PATH App source checkout to mount - --app-id ID Nextcloud app id - --version Show NCDD version + cat <<'EOF' +Usage: ncdd [options] Commands: - up Start the disposable development stack - down Stop and remove the stack - status Show compose service status - env Show the resolved NCDD environment - doctor [--json] Validate Docker, stack and Nextcloud readiness - shell Open a shell in the development worker + up Start the existing NCDD stack + down Stop the existing NCDD stack + status Show stack status + env Show resolved NCDD environment + doctor [--json] Check Docker, Compose, app service, and Nextcloud + shell [--as USER] Open a shell in the app container exec [--as USER] -- COMMAND... - Execute a command in the development worker - help [ai] Show help; `help ai` prints the agent contract -USAGE + Execute a command in the app container + help [ai] Show help or the compact agent contract +EOF } agent_help() { - cat <<'EOF_AGENT' + cat <<'EOF' NCDD agent contract -- Use `ncdd up`, `ncdd status`, `ncdd doctor --json`, `ncdd exec`, `ncdd test`, and `ncdd down`. -- Do not call docker compose directly unless debugging NCDD itself. -- Do not install PHP, Composer, Node, or Nextcloud on the host. -- Keep private Git authentication on the host/CI checkout; mount the checkout with `--source`. -- Use `--as runtime` for commands that must run as the Nextcloud runtime user. -EOF_AGENT +- Use bin/ncdd instead of calling docker compose directly. +- The CLI operates docker-compose.yml plus docker-compose-postgres.yml. +- Git checkout/authentication stays on the host or CI workspace. +- App worktrees belong under volumes/nextcloud/apps-extra/. +- Use --as runtime for commands that must run as the Nextcloud runtime user. +EOF } fail() { @@ -51,243 +38,145 @@ fail() { exit 1 } -yaml_get() { - local key=$1 file=$2 - [ -f "$file" ] || return 1 - awk -v wanted="$key" ' - function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); return s } - function unquote(s) { - s=trim(s) - if ((substr(s,1,1)=="\"" && substr(s,length(s),1)=="\"") || (substr(s,1,1)=="\047" && substr(s,length(s),1)=="\047")) { - return substr(s,2,length(s)-2) - } - return s - } - /^[[:space:]]*#/ || /^[[:space:]]*$/ { next } - { - match($0, /^[ ]*/); spaces=RLENGTH - level=int(spaces/2)+1 - line=substr($0, spaces+1) - pos=index(line, ":") - if (!pos) next - k=trim(substr(line,1,pos-1)) - v=trim(substr(line,pos+1)) - keys[level]=k - for (i=level+1;i<=12;i++) delete keys[i] - path=keys[1] - for (i=2;i<=level;i++) path=path "." keys[i] - if (path==wanted && v!="") { print unquote(v); exit } - } - ' "$file" -} - -abs_path() { - local path=$1 base=$2 - if [[ "$path" = /* ]]; then - printf '%s\n' "$path" - else - (cd "$base" && cd "$path" && pwd) - fi -} - -normalize_nextcloud() { - local input=${1:-} - case "$input" in - '') printf 'stable35\n' ;; - stable[0-9]*) printf '%s\n' "$input" ;; - [0-9]*) printf 'stable%s\n' "$input" ;; - main|master) printf '%s\n' "$input" ;; - *) fail "unsupported Nextcloud ref: $input" ;; - esac -} - -nextcloud_major() { - case "$1" in - stable[0-9]*) printf '%s\n' "${1#stable}" ;; - [0-9]*) printf '%s\n' "$1" ;; - main|master) printf 'main\n' ;; - *) fail "cannot determine Nextcloud major from $1" ;; - esac -} - -sanitize_project_name() { - printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_-]+/-/g; s/^-+//; s/-+$//' +compose() { + docker compose \ + --project-directory "$NCDD_ROOT" \ + -f "$NCDD_ROOT/docker-compose.yml" \ + -f "$NCDD_ROOT/docker-compose-postgres.yml" \ + "$@" } -while [ "$#" -gt 0 ]; do - case "$1" in - --project-root) PROJECT_ROOT=${2:?}; shift 2 ;; - --config) CONFIG_FILE=${2:?}; shift 2 ;; - --nextcloud) NEXTCLOUD_INPUT=${2:?}; shift 2 ;; - --source) APP_SOURCE=${2:?}; shift 2 ;; - --app-id) APP_ID=${2:?}; shift 2 ;; - --version) printf 'ncdd %s\n' "$NCDD_VERSION"; exit 0 ;; - -h|--help) usage; exit 0 ;; - --) shift; break ;; - -*) break ;; - *) break ;; - esac -done - -COMMAND=${1:-help} -[ "$#" -gt 0 ] && shift || true - -PROJECT_ROOT=$(abs_path "$PROJECT_ROOT" "$PWD") -if [ -z "$CONFIG_FILE" ]; then - CONFIG_FILE="$PROJECT_ROOT/.ncdd.yml" -elif [[ "$CONFIG_FILE" != /* ]]; then - CONFIG_FILE="$PROJECT_ROOT/$CONFIG_FILE" -fi - -cfg() { yaml_get "$1" "$CONFIG_FILE" 2>/dev/null || true; } - -[ -n "$NEXTCLOUD_INPUT" ] || NEXTCLOUD_INPUT=$(cfg nextcloud.default) -NEXTCLOUD_REF=$(normalize_nextcloud "$NEXTCLOUD_INPUT") -NEXTCLOUD_MAJOR=$(nextcloud_major "$NEXTCLOUD_REF") - -[ -n "$APP_ID" ] || APP_ID=$(cfg app.id) -[ -n "$APP_ID" ] || APP_ID=$(basename "$PROJECT_ROOT") -[ -n "$APP_SOURCE" ] || APP_SOURCE=$(cfg app.source) -[ -n "$APP_SOURCE" ] || APP_SOURCE='.' -APP_SOURCE=$(abs_path "$APP_SOURCE" "$PROJECT_ROOT") -APP_PATH=$(cfg app.path) -[ -n "$APP_PATH" ] || APP_PATH="/var/www/html/apps-extra/$APP_ID" -RUNTIME_USER=$(cfg app.runtime_user) -[ -n "$RUNTIME_USER" ] || RUNTIME_USER=www-data -APP_SERVICE=$(cfg services.app) -[ -n "$APP_SERVICE" ] || APP_SERVICE=app -WORKER_SERVICE=$(cfg services.worker) -[ -n "$WORKER_SERVICE" ] || WORKER_SERVICE=dev-worker -COMPOSE_FILE=$(cfg compose.file) -[ -n "$COMPOSE_FILE" ] || COMPOSE_FILE="$NCDD_ROOT/docker-compose.dev.yml" -if [[ "$COMPOSE_FILE" != /* ]]; then COMPOSE_FILE="$PROJECT_ROOT/$COMPOSE_FILE"; fi -HTTP_PORT=$(cfg http.port) -[ -n "$HTTP_PORT" ] || HTTP_PORT=${NCDD_HTTP_PORT:-8080} -PROJECT_NAME=$(sanitize_project_name "ncdd-${APP_ID}-${NEXTCLOUD_MAJOR}") -APP_IMAGE=${NCDD_APP_IMAGE:-ghcr.io/librecodecoop/nextcloud-docker-app:${NEXTCLOUD_MAJOR}} - -export NCDD_PROJECT_ROOT="$PROJECT_ROOT" -export NCDD_APP_SOURCE="$APP_SOURCE" -export NCDD_APP_ID="$APP_ID" -export NCDD_APP_PATH="$APP_PATH" -export NCDD_RUNTIME_USER="$RUNTIME_USER" -export NCDD_NEXTCLOUD_REF="$NEXTCLOUD_REF" -export NCDD_NEXTCLOUD_MAJOR="$NEXTCLOUD_MAJOR" -export NCDD_HTTP_PORT="$HTTP_PORT" -export NCDD_APP_IMAGE="$APP_IMAGE" - -compose() { - docker compose --project-name "$PROJECT_NAME" -f "$COMPOSE_FILE" "$@" +ensure_network() { + local network=$1 + docker network inspect "$network" >/dev/null 2>&1 || docker network create "$network" >/dev/null } resolve_user() { case "$1" in runtime) printf '%s\n' "$RUNTIME_USER" ;; - developer|root) printf 'root\n' ;; + root) printf 'root\n' ;; *) printf '%s\n' "$1" ;; esac } -print_env() { - cat </dev/null 2>&1 && docker_ok=true - if $docker_ok && docker compose version >/dev/null 2>&1; then compose_ok=true; fi - if $compose_ok && compose ps --status running --services 2>/dev/null | grep -qx "$APP_SERVICE"; then app_running=true; fi - if $app_running && compose exec -T --user "$RUNTIME_USER" "$APP_SERVICE" php occ status --output=json >/dev/null 2>&1; then nc_ready=true; fi + if $docker_ok && docker compose version >/dev/null 2>&1; then + compose_ok=true + fi + if $compose_ok && compose ps --status running --services 2>/dev/null | grep -qx "$APP_SERVICE"; then + app_running=true + fi + if $app_running && compose exec -T --user "$RUNTIME_USER" "$APP_SERVICE" php occ status --output=json >/dev/null 2>&1; then + nextcloud_ready=true + fi local ready=false - if $docker_ok && $compose_ok && $app_running && $nc_ready; then ready=true; fi + if $docker_ok && $compose_ok && $app_running && $nextcloud_ready; then + ready=true + fi if $json; then - printf '{"ready":%s,"docker":%s,"compose":%s,"nextcloud":{"ref":"%s","major":"%s","ready":%s},"app":{"id":"%s","source":"%s","path":"%s","running":%s}}\n' \ - "$ready" "$docker_ok" "$compose_ok" "$NEXTCLOUD_REF" "$NEXTCLOUD_MAJOR" "$nc_ready" \ - "$APP_ID" "$APP_SOURCE" "$APP_PATH" "$app_running" + printf '{"ready":%s,"docker":%s,"compose":%s,"app_running":%s,"nextcloud_ready":%s,"nextcloud_version":"%s"}\n' \ + "$ready" "$docker_ok" "$compose_ok" "$app_running" "$nextcloud_ready" "$(nextcloud_version)" else - printf '%-20s %s\n' 'Docker' "$docker_ok" - printf '%-20s %s\n' 'Docker Compose' "$compose_ok" - printf '%-20s %s\n' 'Nextcloud ref' "$NEXTCLOUD_REF" - printf '%-20s %s\n' 'Nextcloud ready' "$nc_ready" - printf '%-20s %s\n' 'App' "$APP_ID" - printf '%-20s %s\n' 'App source' "$APP_SOURCE" - printf '%-20s %s\n' 'App running' "$app_running" + printf '%-20s %s\n' "Docker" "$docker_ok" + printf '%-20s %s\n' "Docker Compose" "$compose_ok" + printf '%-20s %s\n' "App running" "$app_running" + printf '%-20s %s\n' "Nextcloud ready" "$nextcloud_ready" + printf '%-20s %s\n' "Nextcloud version" "$(nextcloud_version)" fi + $ready } -case "$COMMAND" in +command=${1:-help} +[ "$#" -gt 0 ] && shift || true + +case "$command" in help) - if [ "${1:-}" = ai ]; then agent_help; else usage; fi + if [ "${1:-}" = "ai" ]; then + agent_help + else + usage + fi ;; env) - print_env + printf 'NCDD_ROOT=%s\n' "$NCDD_ROOT" + printf 'NCDD_APP_SERVICE=%s\n' "$APP_SERVICE" + printf 'NCDD_RUNTIME_USER=%s\n' "$RUNTIME_USER" + printf 'NEXTCLOUD_VERSION=%s\n' "$(nextcloud_version)" ;; up) - while [ "$#" -gt 0 ]; do - case "$1" in - --nextcloud) - NEXTCLOUD_REF=$(normalize_nextcloud "${2:?}") - NEXTCLOUD_MAJOR=$(nextcloud_major "$NEXTCLOUD_REF") - PROJECT_NAME=$(sanitize_project_name "ncdd-${APP_ID}-${NEXTCLOUD_MAJOR}") - APP_IMAGE=${NCDD_APP_IMAGE:-ghcr.io/librecodecoop/nextcloud-docker-app:${NEXTCLOUD_MAJOR}} - export NCDD_NEXTCLOUD_REF NCDD_NEXTCLOUD_MAJOR NCDD_APP_IMAGE - shift 2 - ;; - --source) - APP_SOURCE=$(abs_path "${2:?}" "$PROJECT_ROOT") - export NCDD_APP_SOURCE="$APP_SOURCE" - shift 2 - ;; - *) fail "unknown up option: $1" ;; - esac - done - compose up -d --remove-orphans + [ "$#" -eq 0 ] || fail "up does not accept arguments" + ensure_network reverse-proxy + ensure_network postgres + compose up -d redis postgres app web cron ;; down) - compose down --remove-orphans "$@" + compose down "$@" ;; status) compose ps "$@" ;; - doctor) - doctor "$@" - ;; shell) user=root - if [ "${1:-}" = "--as" ]; then user=$(resolve_user "${2:?}"); shift 2; fi + if [ "${1:-}" = "--as" ]; then + user=$(resolve_user "${2:?missing user}") + shift 2 + fi [ "$#" -eq 0 ] || fail "unknown shell arguments: $*" - compose exec --user "$user" "$WORKER_SERVICE" bash + compose exec --user "$user" "$APP_SERVICE" bash ;; exec) user=root - if [ "${1:-}" = "--as" ]; then user=$(resolve_user "${2:?}"); shift 2; fi - if [ "${1:-}" = "--" ]; then shift; fi + if [ "${1:-}" = "--as" ]; then + user=$(resolve_user "${2:?missing user}") + shift 2 + fi + if [ "${1:-}" = "--" ]; then + shift + fi [ "$#" -gt 0 ] || fail "exec requires a command" tty=() - if [ ! -t 0 ] || [ ! -t 1 ]; then tty=(-T); fi - compose exec "${tty[@]}" --user "$user" "$WORKER_SERVICE" "$@" + if [ ! -t 0 ] || [ ! -t 1 ]; then + tty=(-T) + fi + compose exec "${tty[@]}" --user "$user" "$APP_SERVICE" "$@" + ;; + doctor) + doctor "$@" ;; *) - fail "unknown command: $COMMAND" + fail "unknown command: $command" ;; esac diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 4ca2353..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: ${NCDD_PROJECT_NAME:-ncdd} - -services: - db: - image: postgres:16-alpine - environment: - POSTGRES_DB: ${NCDD_POSTGRES_DB:-nextcloud} - POSTGRES_USER: ${NCDD_POSTGRES_USER:-nextcloud} - POSTGRES_PASSWORD: ${NCDD_POSTGRES_PASSWORD:-nextcloud} - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${NCDD_POSTGRES_USER:-nextcloud} -d ${NCDD_POSTGRES_DB:-nextcloud}"] - interval: 2s - timeout: 3s - retries: 30 - volumes: - - db:/var/lib/postgresql/data - - redis: - image: redis:7-alpine - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 2s - timeout: 3s - retries: 30 - - app: - image: ${NCDD_APP_IMAGE:-ghcr.io/librecodecoop/nextcloud-docker-app:35} - environment: - POSTGRES_DB: ${NCDD_POSTGRES_DB:-nextcloud} - POSTGRES_USER: ${NCDD_POSTGRES_USER:-nextcloud} - POSTGRES_PASSWORD: ${NCDD_POSTGRES_PASSWORD:-nextcloud} - POSTGRES_HOST: db - REDIS_HOST: redis - NEXTCLOUD_ADMIN_USER: ${NCDD_ADMIN_USER:-admin} - NEXTCLOUD_ADMIN_PASSWORD: ${NCDD_ADMIN_PASSWORD:-admin} - NEXTCLOUD_TRUSTED_DOMAINS: ${NCDD_TRUSTED_DOMAINS:-localhost 127.0.0.1 web} - depends_on: - db: - condition: service_healthy - redis: - condition: service_healthy - volumes: - - nextcloud:/var/www/html - - ${NCDD_APP_SOURCE:?set NCDD_APP_SOURCE}:${NCDD_APP_PATH:?set NCDD_APP_PATH} - - web: - build: - context: .docker/web - ports: - - "${NCDD_HTTP_PORT:-8080}:80" - depends_on: - - app - volumes: - - nextcloud:/var/www/html:ro - - ${NCDD_APP_SOURCE:?set NCDD_APP_SOURCE}:${NCDD_APP_PATH:?set NCDD_APP_PATH}:ro - - dev-worker: - build: - context: . - dockerfile: .docker/dev-worker/Dockerfile - args: - NCDD_APP_IMAGE: ${NCDD_APP_IMAGE:-ghcr.io/librecodecoop/nextcloud-docker-app:35} - entrypoint: ["/bin/sh", "-c", "trap : TERM INT; sleep infinity & wait"] - working_dir: ${NCDD_APP_PATH:?set NCDD_APP_PATH} - environment: - BEHAT_ROOT_DIR: /var/www/html - BEHAT_RUN_AS: ${NCDD_RUNTIME_USER:-www-data} - BEHAT_VERBOSE: "1" - depends_on: - - app - volumes: - - nextcloud:/var/www/html - - ${NCDD_APP_SOURCE:?set NCDD_APP_SOURCE}:${NCDD_APP_PATH:?set NCDD_APP_PATH} - - node-worker: - image: node:22-bookworm - entrypoint: ["/bin/sh", "-c", "trap : TERM INT; sleep infinity & wait"] - working_dir: /workspace/app - volumes: - - ${NCDD_APP_SOURCE:?set NCDD_APP_SOURCE}:/workspace/app - -volumes: - db: - nextcloud: diff --git a/docs/ncdd.md b/docs/ncdd.md index e928084..112536f 100644 --- a/docs/ncdd.md +++ b/docs/ncdd.md @@ -1,64 +1,61 @@ -# NCDD development interface +# NCDD command interface -NCDD provides one interface for local development, AI agents, CI, Dev Containers, and Codespaces. Docker Compose is an implementation detail behind the `ncdd` command. +NCDD should expose a stable command surface without creating a second development stack. -## Quick start +## Architecture -From an app checkout, copy `.ncdd.yml.example` to `.ncdd.yml` and adjust the app id if necessary. Then run: +`bin/ncdd` is a thin orchestration layer over the files that already define this repository: + +- `docker-compose.yml` +- `docker-compose-postgres.yml` +- `.env` / `.env.example` + +It must not duplicate the stack, own a second version source, or redefine image versions. + +The Makefile remains focused on repository maintenance tasks. It only exposes `test-ncdd` as a convenience for running the CLI test suite; runtime behavior belongs to `bin/ncdd`. + +## Commands ```bash -/path/to/nextcloud-docker/bin/ncdd up --nextcloud 35 -/path/to/nextcloud-docker/bin/ncdd doctor -/path/to/nextcloud-docker/bin/ncdd shell -/path/to/nextcloud-docker/bin/ncdd down +bin/ncdd up +bin/ncdd status +bin/ncdd doctor +bin/ncdd doctor --json +bin/ncdd shell +bin/ncdd exec --as runtime -- php occ status +bin/ncdd down ``` -`35` and `stable35` resolve to the same Nextcloud ref. The source checkout is mounted into the environment; private Git authentication never needs to be copied into a container. - -## Project contract - -`.ncdd.yml` supports the following scalar settings: - -```yaml -nextcloud: - default: stable35 -app: - id: libresign - source: . - path: /var/www/html/apps-extra/libresign - runtime_user: www-data -services: - app: app - worker: dev-worker -http: - port: 8080 -``` +`runtime` resolves to `www-data` by default and can be overridden with `NCDD_RUNTIME_USER`. -Environment variables and command-line flags can override the contract. `NCDD_APP_IMAGE` can select a prebuilt Nextcloud development image explicitly. +## Git and app source -## Diagnostics +NCDD does not fetch application repositories and does not receive GitHub credentials. Human users, CI, and agents perform the checkout outside the containers. -`ncdd env` prints the fully resolved environment. `ncdd doctor` checks Docker, Compose, the app service, and `occ status`. Agents and CI should prefer `ncdd doctor --json`. +Application worktrees should be placed under the existing Nextcloud tree: -## Execution users +```text +volumes/nextcloud/apps-extra/ +``` -`ncdd exec` defaults to the development worker. Use aliases instead of implementation-specific usernames: +That keeps public and private branches identical from NCDD's point of view and avoids container-specific Git authentication. -```bash -ncdd exec -- composer dump-autoload -ncdd exec --as runtime -- php occ status -ncdd exec --as root -- id -``` +## Version source + +There is no NCDD version constant in the CLI. The Nextcloud image version is read from the existing `NEXTCLOUD_VERSION` environment variable, then `.env`, then `.env.example`. -`runtime` resolves to `app.runtime_user` (normally `www-data`). +A separate NCDD release/version mechanism should only be introduced if the project actually starts publishing the CLI independently. -## Private branches and forks +## Testing -Check out private code on the host or in the CI workspace and mount it: +The CLI is Bash, so its behavior is covered with Bats rather than ad-hoc shell assertions. CI also runs ShellCheck. ```bash -gh repo clone OWNER/PRIVATE-FORK app -ncdd --project-root app --source app up --nextcloud stable35 +make test-ncdd ``` -Do not run `git fetch` for private repositories from inside the runtime containers. +The GitHub workflow pins the Bats setup action to an immutable commit SHA and pins the Bats version explicitly. + +## Configuration policy + +PR #59 intentionally does not introduce `.ncdd.yml`. Environment-specific project contracts may become useful for test-suite definitions later, but adding another configuration format before there is a concrete consumer requirement would duplicate information already present in Compose and `.env`. diff --git a/tests/ncdd.bats b/tests/ncdd.bats new file mode 100644 index 0000000..6c3dec6 --- /dev/null +++ b/tests/ncdd.bats @@ -0,0 +1,81 @@ +#!/usr/bin/env bats + +setup() { + REPO_ROOT=$(cd "$BATS_TEST_DIRNAME/.." && pwd) + TMP_ROOT=$(mktemp -d) + mkdir -p "$TMP_ROOT/bin" + export NCDD_TEST_LOG="$TMP_ROOT/docker.log" + + cat > "$TMP_ROOT/bin/docker" <<'MOCK' +#!/usr/bin/env bash +set -euo pipefail +printf '%q ' "$@" >> "${NCDD_TEST_LOG:?}" +printf '\n' >> "${NCDD_TEST_LOG:?}" + +if [ "${1:-}" = "compose" ] && [ "${2:-}" = "version" ]; then + exit 0 +fi + +case " $* " in + *" ps --status running --services "*) + printf 'app\nredis\npostgres\n' + ;; + *" php occ status --output=json "*) + printf '{"installed":true}\n' + ;; +esac +MOCK + chmod +x "$TMP_ROOT/bin/docker" + export PATH="$TMP_ROOT/bin:$PATH" +} + +teardown() { + rm -rf "$TMP_ROOT" +} + +@test "help documents the supported interface" { + run bash "$REPO_ROOT/bin/ncdd" help + [ "$status" -eq 0 ] + [[ "$output" == *"doctor [--json]"* ]] + [[ "$output" == *"exec [--as USER]"* ]] +} + +@test "env reads the existing repository version source" { + run bash "$REPO_ROOT/bin/ncdd" env + [ "$status" -eq 0 ] + [[ "$output" == *"NEXTCLOUD_VERSION=34-fpm"* ]] +} + +@test "doctor exposes a machine-readable healthy state" { + run bash "$REPO_ROOT/bin/ncdd" doctor --json + [ "$status" -eq 0 ] + [[ "$output" == *'"ready":true'* ]] + [[ "$output" == *'"nextcloud_ready":true'* ]] +} + +@test "runtime alias maps to www-data in the existing app service" { + run bash "$REPO_ROOT/bin/ncdd" exec --as runtime -- php -v + [ "$status" -eq 0 ] + grep -q -- '--user www-data app php -v' "$NCDD_TEST_LOG" +} + +@test "up uses the existing compose files and required external networks" { + run bash "$REPO_ROOT/bin/ncdd" up + [ "$status" -eq 0 ] + grep -q 'network inspect reverse-proxy' "$NCDD_TEST_LOG" + grep -q 'network inspect postgres' "$NCDD_TEST_LOG" + grep -q -- "-f $REPO_ROOT/docker-compose.yml" "$NCDD_TEST_LOG" + grep -q -- "-f $REPO_ROOT/docker-compose-postgres.yml" "$NCDD_TEST_LOG" + grep -q 'up -d redis postgres app web cron' "$NCDD_TEST_LOG" +} + +@test "agent help keeps Docker Compose as an implementation detail" { + run bash "$REPO_ROOT/bin/ncdd" help ai + [ "$status" -eq 0 ] + [[ "$output" == *"Use bin/ncdd instead of calling docker compose directly."* ]] +} + +@test "CLI is valid Bash" { + run bash -n "$REPO_ROOT/bin/ncdd" + [ "$status" -eq 0 ] +} diff --git a/tests/test-ncdd.sh b/tests/test-ncdd.sh deleted file mode 100644 index cd3b7c8..0000000 --- a/tests/test-ncdd.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -tmp=$(mktemp -d) -trap 'rm -rf "$tmp"' EXIT -mkdir -p "$tmp/project" "$tmp/bin" - -cat > "$tmp/project/.ncdd.yml" <<'YAML' -nextcloud: - default: 35 -app: - id: demo - source: . - path: /var/www/html/apps-extra/demo - runtime_user: www-data -services: - app: app - worker: dev-worker -http: - port: 8888 -YAML - -cat > "$tmp/bin/docker" <<'MOCK' -#!/usr/bin/env bash -set -euo pipefail -printf '%q ' "$@" >> "${NCDD_TEST_LOG:?}" -printf '\n' >> "${NCDD_TEST_LOG:?}" -if [ "${1:-}" = compose ] && [ "${2:-}" = version ]; then exit 0; fi -case " $* " in - *" ps --status running --services "*) printf 'app\ndev-worker\n'; exit 0 ;; - *" php occ status --output=json "*) printf '{"installed":true}\n'; exit 0 ;; -esac -exit 0 -MOCK -chmod +x "$tmp/bin/docker" - -export PATH="$tmp/bin:$PATH" -export NCDD_TEST_LOG="$tmp/docker.log" - -out=$(cd "$tmp/project" && "$repo_root/bin/ncdd" env) -grep -q '^NCDD_NEXTCLOUD_REF=stable35$' <<<"$out" -grep -q '^NCDD_NEXTCLOUD_MAJOR=35$' <<<"$out" -grep -q '^NCDD_APP_ID=demo$' <<<"$out" -grep -q '^NCDD_HTTP_PORT=8888$' <<<"$out" - -json=$(cd "$tmp/project" && "$repo_root/bin/ncdd" doctor --json) -grep -q '"ready":true' <<<"$json" -grep -q '"ref":"stable35"' <<<"$json" - -(cd "$tmp/project" && "$repo_root/bin/ncdd" exec --as runtime -- php -v) -grep -q -- '--user www-data dev-worker php -v' "$tmp/docker.log" - -help=$("$repo_root/bin/ncdd" help ai) -grep -q 'Do not call docker compose directly' <<<"$help" - -bash -n "$repo_root/bin/ncdd" -echo 'ncdd CLI tests passed' From bacc6ed5f98d73ba0ccc9a3e3cd2d0ada1d62ed1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:16:59 +0000 Subject: [PATCH 4/7] ci(ncdd): let bats action select current bats Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/ncdd-cli.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ncdd-cli.yml b/.github/workflows/ncdd-cli.yml index a13ee15..77f691e 100644 --- a/.github/workflows/ncdd-cli.yml +++ b/.github/workflows/ncdd-cli.yml @@ -33,7 +33,6 @@ jobs: - name: Setup Bats uses: bats-core/bats-action@77d6fb60505b4d0d1d73e48bd035b55074bbfb43 # 4.0.0 with: - bats-version: 1.14.0 support-install: false assert-install: false detik-install: false From 6c414cc89e7b19cc65d41c3b82cbf20676c61748 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:38:14 +0000 Subject: [PATCH 5/7] fix(ncdd): satisfy shellcheck network guard Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- bin/ncdd | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bin/ncdd b/bin/ncdd index 8021198..49e639b 100644 --- a/bin/ncdd +++ b/bin/ncdd @@ -48,7 +48,10 @@ compose() { ensure_network() { local network=$1 - docker network inspect "$network" >/dev/null 2>&1 || docker network create "$network" >/dev/null + + if ! docker network inspect "$network" >/dev/null 2>&1; then + docker network create "$network" >/dev/null + fi } resolve_user() { @@ -179,4 +182,4 @@ case "$command" in *) fail "unknown command: $command" ;; -esac +esac \ No newline at end of file From bd5871fa06fa1c36f2b133d4bae7794523234014 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:39:46 +0000 Subject: [PATCH 6/7] fix(ncdd): avoid ambiguous shell conditional Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- bin/ncdd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/ncdd b/bin/ncdd index 49e639b..ee9435c 100644 --- a/bin/ncdd +++ b/bin/ncdd @@ -123,7 +123,9 @@ doctor() { } command=${1:-help} -[ "$#" -gt 0 ] && shift || true +if [ "$#" -gt 0 ]; then + shift +fi case "$command" in help) From a7ff897dac120f4dcd3db0f831ff13713975512e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:24:52 +0000 Subject: [PATCH 7/7] chore(ncdd): align branch with fixed web image Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/web/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.docker/web/Dockerfile b/.docker/web/Dockerfile index d8dffd2..b719fcc 100644 --- a/.docker/web/Dockerfile +++ b/.docker/web/Dockerfile @@ -1,4 +1,6 @@ FROM nginx:alpine +RUN apk upgrade --no-cache + COPY nginx.conf /etc/nginx/nginx.conf COPY nextcloud.conf /etc/nginx/nextcloud.conf