feat: add discourse-cli for Discourse forum integration
A standalone CLI container (following the redmine-cli pattern) that
lets AI agents and humans read, post, reply, search, and discuss on a
Discourse forum via the REST API. Uses three environment variables
(DISCOURSE_URL, DISCOURSE_API_KEY, DISCOURSE_API_USERNAME) for auth.
Commands: whoami, categories, cat-info, topics/ls, show, create, reply,
update, delete, search, notifications.
Fully validated end-to-end (10/10 checks) against a live Discourse
instance: credential auth, whoami, categories, topics, show, search,
and full write cycle (create + reply + update + delete).
Includes validate.sh for repeatable end-to-end testing using
containerized curl + the CLI image.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
Executable
+209
@@ -0,0 +1,209 @@
|
||||
#!/bin/sh
|
||||
# discourse-cli validation script.
|
||||
# Runs everything inside containers so no host tooling (curl, etc.) is needed.
|
||||
#
|
||||
# Usage: ./validate.sh (reads .env from the discourse-cli directory)
|
||||
#
|
||||
# Exit codes: 0 = all checks passed, 1 = one or more checks failed
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Ensure .env exists
|
||||
if [ ! -f .env ]; then
|
||||
echo "FAIL: .env not found. Copy .env.example to .env and fill in real values."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGE="${DISCOURSE_CLI_IMAGE:-kneldevstack-aimiddleware-discourse-cli}"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
TOTAL=0
|
||||
|
||||
ok() { TOTAL=$((TOTAL+1)); echo "PASS: $1"; PASS=$((PASS+1)); }
|
||||
fail() { TOTAL=$((TOTAL+1)); echo "FAIL: $1"; FAIL=$((FAIL+1)); }
|
||||
|
||||
echo "============================================"
|
||||
echo " discourse-cli validation"
|
||||
echo " image: ${IMAGE}"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------------------------- #
|
||||
# 0. Credential sanity (raw HTTP via containerized curl)
|
||||
# ---------------------------------------------------------------- #
|
||||
echo "--- [0] Credential check (raw HTTP via containerized curl) ---"
|
||||
|
||||
# Load env vars from .env for the raw test
|
||||
DISCOURSE_URL=$(grep -E '^DISCOURSE_URL=' .env | cut -d= -f2-)
|
||||
DISCOURSE_API_KEY=$(grep -E '^DISCOURSE_API_KEY=' .env | cut -d= -f2-)
|
||||
DISCOURSE_API_USERNAME=$(grep -E '^DISCOURSE_API_USERNAME=' .env | cut -d= -f2-)
|
||||
|
||||
HTTP_CODE=$(docker run --rm curlimages/curl:latest \
|
||||
-s -o /dev/null -w '%{http_code}' \
|
||||
-H "Api-Key: ${DISCOURSE_API_KEY}" \
|
||||
-H "Api-Username: ${DISCOURSE_API_USERNAME}" \
|
||||
-H "Accept: application/json" \
|
||||
"${DISCOURSE_URL}/session/current.json" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
ok "raw HTTP credentials accepted (HTTP ${HTTP_CODE})"
|
||||
else
|
||||
fail "raw HTTP credentials rejected (HTTP ${HTTP_CODE})"
|
||||
echo " -> Discourse at ${DISCOURSE_URL} returned ${HTTP_CODE} for /session/current.json"
|
||||
echo " -> Check: API key active? IP allowlist? Username matches key scope?"
|
||||
echo ""
|
||||
echo " Stopping: cannot validate CLI commands without valid credentials."
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " RESULTS: ${PASS}/${TOTAL} passed, ${FAIL} failed"
|
||||
echo "============================================"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------------------------- #
|
||||
# 1. whoami (live connection test)
|
||||
# ---------------------------------------------------------------- #
|
||||
echo "--- [1] whoami ---"
|
||||
OUT=$(docker run --rm --env-file .env "${IMAGE}" whoami 2>&1) || true
|
||||
if echo "$OUT" | grep -qE '^username:'; then
|
||||
ok "whoami returned user info"
|
||||
echo " $OUT" | head -5
|
||||
else
|
||||
fail "whoami did not return expected output"
|
||||
echo " $OUT" | head -5
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------------------------- #
|
||||
# 2. categories (live read)
|
||||
# ---------------------------------------------------------------- #
|
||||
echo "--- [2] categories ---"
|
||||
OUT=$(docker run --rm --env-file .env "${IMAGE}" categories 2>&1) || true
|
||||
if echo "$OUT" | grep -qE '^[0-9]+ category'; then
|
||||
ok "categories returned data"
|
||||
echo " $OUT" | head -6
|
||||
# Grab first category id for later tests
|
||||
CAT_ID=$(echo "$OUT" | grep -E '^[0-9]' | head -1 | awk '{print $1}')
|
||||
else
|
||||
fail "categories did not return expected output"
|
||||
echo " $OUT" | head -6
|
||||
CAT_ID=""
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------------------------- #
|
||||
# 3. topics / ls (live read)
|
||||
# ---------------------------------------------------------------- #
|
||||
echo "--- [3] topics (latest) ---"
|
||||
OUT=$(docker run --rm --env-file .env "${IMAGE}" ls 2>&1) || true
|
||||
if echo "$OUT" | grep -qE 'topic\(s\)'; then
|
||||
ok "topics list returned data"
|
||||
echo " $OUT" | head -6
|
||||
TOPIC_ID=$(echo "$OUT" | grep -E '^[0-9]' | head -1 | awk '{print $1}')
|
||||
else
|
||||
fail "topics list did not return expected output"
|
||||
echo " $OUT" | head -6
|
||||
TOPIC_ID=""
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------------------------- #
|
||||
# 4. show topic (live read)
|
||||
# ---------------------------------------------------------------- #
|
||||
if [ -n "${TOPIC_ID:-}" ]; then
|
||||
echo "--- [4] show topic ${TOPIC_ID} ---"
|
||||
OUT=$(docker run --rm --env-file .env "${IMAGE}" show "${TOPIC_ID}" 2>&1) || true
|
||||
if echo "$OUT" | grep -qE '^#'; then
|
||||
ok "show topic ${TOPIC_ID} returned content"
|
||||
echo " $OUT" | head -8
|
||||
else
|
||||
fail "show topic ${TOPIC_ID} did not return expected output"
|
||||
echo " $OUT" | head -8
|
||||
fi
|
||||
else
|
||||
echo "--- [4] show topic (SKIPPED - no topic id found in step 3) ---"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------------------------- #
|
||||
# 5. search (live read)
|
||||
# ---------------------------------------------------------------- #
|
||||
echo "--- [5] search ---"
|
||||
OUT=$(docker run --rm --env-file .env "${IMAGE}" search "test" 2>&1) || true
|
||||
if echo "$OUT" | grep -qE 'result\(s\)'; then
|
||||
ok "search returned results"
|
||||
echo " $OUT" | head -5
|
||||
else
|
||||
fail "search did not return expected output"
|
||||
echo " $OUT" | head -5
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------------------------- #
|
||||
# 6. create + reply + update + delete (live write cycle)
|
||||
# ---------------------------------------------------------------- #
|
||||
echo "--- [6] create + reply + update + delete (write cycle) ---"
|
||||
|
||||
CREATE_OUT=$(docker run --rm --env-file .env "${IMAGE}" create \
|
||||
${CAT_ID:+-c "$CAT_ID"} \
|
||||
-t "discourse-cli validation test $(date +%s)" \
|
||||
-b "This is an automated validation test post. It will be cleaned up shortly after creation." \
|
||||
2>&1) || true
|
||||
NEW_TOPIC_ID=$(echo "$CREATE_OUT" | grep -oE 'topic #[0-9]+' | grep -oE '[0-9]+')
|
||||
|
||||
if [ -n "${NEW_TOPIC_ID:-}" ]; then
|
||||
ok "create topic succeeded (topic #${NEW_TOPIC_ID})"
|
||||
echo " $CREATE_OUT"
|
||||
|
||||
# reply
|
||||
REPLY_OUT=$(docker run --rm --env-file .env "${IMAGE}" reply "${NEW_TOPIC_ID}" \
|
||||
-b "Validation reply test." 2>&1) || true
|
||||
POST_ID=$(echo "$REPLY_OUT" | grep -oE 'id=[0-9]+' | grep -oE '[0-9]+')
|
||||
if echo "$REPLY_OUT" | grep -qE 'Posted reply'; then
|
||||
ok "reply succeeded"
|
||||
echo " $REPLY_OUT"
|
||||
else
|
||||
fail "reply failed"
|
||||
echo " $REPLY_OUT"
|
||||
fi
|
||||
|
||||
# update
|
||||
if [ -n "${POST_ID:-}" ]; then
|
||||
UPD_OUT=$(docker run --rm --env-file .env "${IMAGE}" update "${POST_ID}" \
|
||||
-b "Updated validation reply test." 2>&1) || true
|
||||
if echo "$UPD_OUT" | grep -qE 'Updated post'; then
|
||||
ok "update post ${POST_ID} succeeded"
|
||||
else
|
||||
fail "update post ${POST_ID} failed"
|
||||
echo " $UPD_OUT"
|
||||
fi
|
||||
|
||||
# delete
|
||||
DEL_OUT=$(docker run --rm --env-file .env "${IMAGE}" delete "${POST_ID}" 2>&1) || true
|
||||
if echo "$DEL_OUT" | grep -qE 'Deleted post'; then
|
||||
ok "delete post ${POST_ID} succeeded"
|
||||
else
|
||||
fail "delete post ${POST_ID} failed"
|
||||
echo " $DEL_OUT"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
fail "create topic failed"
|
||||
echo " $CREATE_OUT"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------------------------- #
|
||||
# Summary
|
||||
# ---------------------------------------------------------------- #
|
||||
echo "============================================"
|
||||
echo " RESULTS: ${PASS}/${TOTAL} passed, ${FAIL} failed"
|
||||
echo "============================================"
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
Reference in New Issue
Block a user