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 diff --git a/.github/workflows/ncdd-cli.yml b/.github/workflows/ncdd-cli.yml new file mode 100644 index 0000000..77f691e --- /dev/null +++ b/.github/workflows/ncdd-cli.yml @@ -0,0 +1,45 @@ +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: + 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/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..491dd61 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,7 @@ +# NCDD agent contract + +Use `bin/ncdd` for stack operations. Do not reproduce Docker Compose commands in agent instructions unless debugging NCDD itself. + +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 7d27b09..fa9655d 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: + bats tests/ncdd.bats + scan-images: @set -e; \ version="$$(sed -n 's/^NEXTCLOUD_VERSION=//p' .env.example | head -n 1)"; \ diff --git a/README.md b/README.md index 9574476..9d3ff8c 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 command interface + +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 +bin/ncdd up +bin/ncdd doctor +bin/ncdd shell +bin/ncdd down +``` + +See [docs/ncdd.md](docs/ncdd.md) for the command contract and testing policy. + ## 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: diff --git a/bin/ncdd b/bin/ncdd new file mode 100644 index 0000000..ee9435c --- /dev/null +++ b/bin/ncdd @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +set -euo pipefail + +NCDD_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +APP_SERVICE=${NCDD_APP_SERVICE:-app} +RUNTIME_USER=${NCDD_RUNTIME_USER:-www-data} + +usage() { + cat <<'EOF' +Usage: ncdd [options] + +Commands: + 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 app container + help [ai] Show help or the compact agent contract +EOF +} + +agent_help() { + cat <<'EOF' +NCDD agent contract +- 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() { + printf 'ncdd: %s\n' "$*" >&2 + exit 1 +} + +compose() { + docker compose \ + --project-directory "$NCDD_ROOT" \ + -f "$NCDD_ROOT/docker-compose.yml" \ + -f "$NCDD_ROOT/docker-compose-postgres.yml" \ + "$@" +} + +ensure_network() { + local network=$1 + + if ! docker network inspect "$network" >/dev/null 2>&1; then + docker network create "$network" >/dev/null + fi +} + +resolve_user() { + case "$1" in + runtime) printf '%s\n' "$RUNTIME_USER" ;; + root) printf 'root\n' ;; + *) printf '%s\n' "$1" ;; + esac +} + +nextcloud_version() { + if [ -n "${NEXTCLOUD_VERSION:-}" ]; then + printf '%s\n' "$NEXTCLOUD_VERSION" + return + fi + + local file + for file in "$NCDD_ROOT/.env" "$NCDD_ROOT/.env.example"; do + if [ -f "$file" ]; then + local value + value=$(sed -n 's/^NEXTCLOUD_VERSION=//p' "$file" | head -n 1) + if [ -n "$value" ]; then + printf '%s\n' "$value" + return + fi + fi + done + + printf 'unset\n' +} + +doctor() { + local json=false + if [ "${1:-}" = "--json" ]; then + json=true + shift + fi + [ "$#" -eq 0 ] || fail "doctor accepts only --json" + + local docker_ok=false compose_ok=false app_running=false nextcloud_ready=false + command -v docker >/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 + nextcloud_ready=true + fi + + local ready=false + if $docker_ok && $compose_ok && $app_running && $nextcloud_ready; then + ready=true + fi + + if $json; then + 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' "App running" "$app_running" + printf '%-20s %s\n' "Nextcloud ready" "$nextcloud_ready" + printf '%-20s %s\n' "Nextcloud version" "$(nextcloud_version)" + fi + + $ready +} + +command=${1:-help} +if [ "$#" -gt 0 ]; then + shift +fi + +case "$command" in + help) + if [ "${1:-}" = "ai" ]; then + agent_help + else + usage + fi + ;; + 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) + [ "$#" -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 "$@" + ;; + status) + compose ps "$@" + ;; + shell) + user=root + if [ "${1:-}" = "--as" ]; then + user=$(resolve_user "${2:?missing user}") + shift 2 + fi + [ "$#" -eq 0 ] || fail "unknown shell arguments: $*" + compose exec --user "$user" "$APP_SERVICE" bash + ;; + exec) + user=root + 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" "$APP_SERVICE" "$@" + ;; + doctor) + doctor "$@" + ;; + *) + fail "unknown command: $command" + ;; +esac \ No newline at end of file diff --git a/docs/ncdd.md b/docs/ncdd.md new file mode 100644 index 0000000..112536f --- /dev/null +++ b/docs/ncdd.md @@ -0,0 +1,61 @@ +# NCDD command interface + +NCDD should expose a stable command surface without creating a second development stack. + +## Architecture + +`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 +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 +``` + +`runtime` resolves to `www-data` by default and can be overridden with `NCDD_RUNTIME_USER`. + +## Git and app source + +NCDD does not fetch application repositories and does not receive GitHub credentials. Human users, CI, and agents perform the checkout outside the containers. + +Application worktrees should be placed under the existing Nextcloud tree: + +```text +volumes/nextcloud/apps-extra/ +``` + +That keeps public and private branches identical from NCDD's point of view and avoids container-specific Git authentication. + +## 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`. + +A separate NCDD release/version mechanism should only be introduced if the project actually starts publishing the CLI independently. + +## Testing + +The CLI is Bash, so its behavior is covered with Bats rather than ad-hoc shell assertions. CI also runs ShellCheck. + +```bash +make test-ncdd +``` + +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 ] +}