fix: Add Dendrite to docker-compose stack (#619)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
df640af7c1
commit
b86edd7e5d
6 changed files with 195 additions and 9 deletions
12
.env.example
12
.env.example
|
|
@ -38,10 +38,14 @@ WOODPECKER_DB_HOST=127.0.0.1 # [CONFIG] Postgres host
|
||||||
WOODPECKER_DB_NAME=woodpecker # [CONFIG] Postgres database name
|
WOODPECKER_DB_NAME=woodpecker # [CONFIG] Postgres database name
|
||||||
|
|
||||||
# ── Matrix (optional — real-time notifications & escalation replies) ──────
|
# ── Matrix (optional — real-time notifications & escalation replies) ──────
|
||||||
MATRIX_HOMESERVER=http://localhost:8008 # [CONFIG] Dendrite/Synapse URL
|
# In compose mode, Dendrite runs inside the Docker network. `disinto init`
|
||||||
MATRIX_BOT_USER=@factory:your.server # [CONFIG] bot's Matrix user ID
|
# provisions the bot user, room, and token automatically.
|
||||||
MATRIX_TOKEN= # [SECRET] bot's access token
|
# Compose: MATRIX_HOMESERVER defaults to http://dendrite:8008 (set by env.sh)
|
||||||
MATRIX_ROOM_ID= # [CONFIG] coordination room ID
|
# Bare metal: MATRIX_HOMESERVER defaults to http://localhost:8008
|
||||||
|
MATRIX_HOMESERVER=http://dendrite:8008 # [CONFIG] Dendrite URL (compose default)
|
||||||
|
MATRIX_BOT_USER=@factory-bot:disinto.local # [CONFIG] bot's Matrix user ID
|
||||||
|
MATRIX_TOKEN= # [SECRET] bot's access token (auto-provisioned)
|
||||||
|
MATRIX_ROOM_ID= # [CONFIG] coordination room ID (auto-provisioned)
|
||||||
|
|
||||||
# ── Project-specific secrets ──────────────────────────────────────────────
|
# ── Project-specific secrets ──────────────────────────────────────────────
|
||||||
# Store all project secrets here so formulas reference env vars, never hardcode.
|
# Store all project secrets here so formulas reference env vars, never hardcode.
|
||||||
|
|
|
||||||
158
bin/disinto
158
bin/disinto
|
|
@ -155,7 +155,7 @@ generate_compose() {
|
||||||
|
|
||||||
cat > "$compose_file" <<'COMPOSEEOF'
|
cat > "$compose_file" <<'COMPOSEEOF'
|
||||||
# docker-compose.yml — generated by disinto init
|
# docker-compose.yml — generated by disinto init
|
||||||
# Brings up Forgejo, Woodpecker, and the agent runtime.
|
# Brings up Forgejo, Woodpecker, Dendrite (Matrix), and the agent runtime.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
forgejo:
|
forgejo:
|
||||||
|
|
@ -194,6 +194,16 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- disinto-net
|
- disinto-net
|
||||||
|
|
||||||
|
dendrite:
|
||||||
|
image: matrixdotorg/dendrite-monolith:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- dendrite-data:/etc/dendrite
|
||||||
|
environment:
|
||||||
|
DENDRITE_DOMAIN: disinto.local
|
||||||
|
networks:
|
||||||
|
- disinto-net
|
||||||
|
|
||||||
agents:
|
agents:
|
||||||
build: ./docker/agents
|
build: ./docker/agents
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
@ -208,18 +218,21 @@ services:
|
||||||
environment:
|
environment:
|
||||||
FORGE_URL: http://forgejo:3000
|
FORGE_URL: http://forgejo:3000
|
||||||
WOODPECKER_SERVER: http://woodpecker:8000
|
WOODPECKER_SERVER: http://woodpecker:8000
|
||||||
|
MATRIX_HOMESERVER: http://dendrite:8008
|
||||||
DISINTO_CONTAINER: "1"
|
DISINTO_CONTAINER: "1"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
depends_on:
|
depends_on:
|
||||||
- forgejo
|
- forgejo
|
||||||
- woodpecker
|
- woodpecker
|
||||||
|
- dendrite
|
||||||
networks:
|
networks:
|
||||||
- disinto-net
|
- disinto-net
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
forgejo-data:
|
forgejo-data:
|
||||||
woodpecker-data:
|
woodpecker-data:
|
||||||
|
dendrite-data:
|
||||||
agent-data:
|
agent-data:
|
||||||
project-repos:
|
project-repos:
|
||||||
claude-auth:
|
claude-auth:
|
||||||
|
|
@ -899,6 +912,144 @@ setup_woodpecker() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Provision Dendrite Matrix homeserver: create bot user, room, and access token.
|
||||||
|
# Stores MATRIX_TOKEN, MATRIX_ROOM_ID, MATRIX_BOT_USER in .env.
|
||||||
|
setup_matrix() {
|
||||||
|
local use_bare="${DISINTO_BARE:-false}"
|
||||||
|
local env_file="${FACTORY_ROOT}/.env"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "── Matrix setup ───────────────────────────────────────"
|
||||||
|
|
||||||
|
# In compose mode, Dendrite runs inside the network at http://dendrite:8008.
|
||||||
|
# For provisioning from the host during init, we exec into the container.
|
||||||
|
local matrix_host="http://dendrite:8008"
|
||||||
|
|
||||||
|
# Skip if MATRIX_TOKEN is already configured
|
||||||
|
if [ -n "${MATRIX_TOKEN:-}" ]; then
|
||||||
|
echo "Matrix: already configured (MATRIX_TOKEN set)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$use_bare" = true ]; then
|
||||||
|
echo "Matrix: skipped in bare mode (configure manually or install Dendrite)"
|
||||||
|
echo " See: https://matrix-org.github.io/dendrite/"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait for Dendrite to become healthy
|
||||||
|
echo -n "Waiting for Dendrite to start"
|
||||||
|
local retries=0
|
||||||
|
while true; do
|
||||||
|
# Probe Dendrite via docker compose exec since it's not exposed on the host
|
||||||
|
local version_resp
|
||||||
|
version_resp=$(docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T dendrite \
|
||||||
|
curl -sf --max-time 3 "http://localhost:8008/_matrix/client/versions" 2>/dev/null) || version_resp=""
|
||||||
|
if [ -n "$version_resp" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
retries=$((retries + 1))
|
||||||
|
if [ "$retries" -gt 60 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Warning: Dendrite did not become ready within 60s — skipping Matrix setup" >&2
|
||||||
|
echo " Run 'disinto init' again after Dendrite is healthy" >&2
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo -n "."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo " ready"
|
||||||
|
|
||||||
|
# Create bot user via Dendrite's create-account tool
|
||||||
|
local bot_localpart="factory-bot"
|
||||||
|
local bot_user="@${bot_localpart}:disinto.local"
|
||||||
|
local bot_pass
|
||||||
|
bot_pass="matrix-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 24)"
|
||||||
|
|
||||||
|
echo "Creating Matrix bot user: ${bot_user}"
|
||||||
|
docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T dendrite \
|
||||||
|
/usr/bin/create-account \
|
||||||
|
-config /etc/dendrite/dendrite.yaml \
|
||||||
|
-username "${bot_localpart}" \
|
||||||
|
-password "${bot_pass}" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Log in to get an access token
|
||||||
|
local login_resp
|
||||||
|
login_resp=$(docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T dendrite \
|
||||||
|
curl -sf -X POST "http://localhost:8008/_matrix/client/v3/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"${bot_localpart}\"},\"password\":\"${bot_pass}\"}" \
|
||||||
|
2>/dev/null) || login_resp=""
|
||||||
|
|
||||||
|
local access_token
|
||||||
|
access_token=$(printf '%s' "$login_resp" | jq -r '.access_token // empty' 2>/dev/null) || access_token=""
|
||||||
|
|
||||||
|
if [ -z "$access_token" ]; then
|
||||||
|
echo "Warning: failed to obtain Matrix access token — skipping Matrix setup" >&2
|
||||||
|
echo " Create the bot user manually via Dendrite admin API" >&2
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Bot login successful"
|
||||||
|
|
||||||
|
# Create coordination room
|
||||||
|
local room_resp
|
||||||
|
room_resp=$(docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T dendrite \
|
||||||
|
curl -sf -X POST "http://localhost:8008/_matrix/client/v3/createRoom" \
|
||||||
|
-H "Authorization: Bearer ${access_token}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"room_alias_name":"factory","name":"disinto factory","topic":"Autonomous code factory coordination room","preset":"private_chat"}' \
|
||||||
|
2>/dev/null) || room_resp=""
|
||||||
|
|
||||||
|
local room_id
|
||||||
|
room_id=$(printf '%s' "$room_resp" | jq -r '.room_id // empty' 2>/dev/null) || room_id=""
|
||||||
|
|
||||||
|
if [ -z "$room_id" ]; then
|
||||||
|
# Room might already exist — try resolving the alias
|
||||||
|
local alias_resp
|
||||||
|
alias_resp=$(docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T dendrite \
|
||||||
|
curl -sf "http://localhost:8008/_matrix/client/v3/directory/room/%23factory%3Adisinto.local" \
|
||||||
|
-H "Authorization: Bearer ${access_token}" \
|
||||||
|
2>/dev/null) || alias_resp=""
|
||||||
|
room_id=$(printf '%s' "$alias_resp" | jq -r '.room_id // empty' 2>/dev/null) || room_id=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$room_id" ]; then
|
||||||
|
echo "Warning: failed to create or find coordination room — skipping Matrix setup" >&2
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Room: ${room_id} (alias: #factory:disinto.local)"
|
||||||
|
|
||||||
|
# Store Matrix credentials in .env
|
||||||
|
local matrix_vars=(
|
||||||
|
"MATRIX_HOMESERVER=${matrix_host}"
|
||||||
|
"MATRIX_BOT_USER=${bot_user}"
|
||||||
|
"MATRIX_TOKEN=${access_token}"
|
||||||
|
"MATRIX_ROOM_ID=${room_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for var_line in "${matrix_vars[@]}"; do
|
||||||
|
local var_name="${var_line%%=*}"
|
||||||
|
if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then
|
||||||
|
sed -i "s|^${var_name}=.*|${var_line}|" "$env_file"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$var_line" >> "$env_file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
export MATRIX_TOKEN="$access_token"
|
||||||
|
export MATRIX_BOT_USER="$bot_user"
|
||||||
|
export MATRIX_ROOM_ID="$room_id"
|
||||||
|
export MATRIX_HOMESERVER="$matrix_host"
|
||||||
|
|
||||||
|
echo " Credentials saved to .env"
|
||||||
|
echo ""
|
||||||
|
echo " To receive notifications in your Matrix client:"
|
||||||
|
echo " 1. Add 'ports: [\"8008:8008\"]' to the dendrite service in docker-compose.yml"
|
||||||
|
echo " 2. Join #factory:disinto.local from Element or another Matrix client"
|
||||||
|
}
|
||||||
|
|
||||||
# ── init command ─────────────────────────────────────────────────────────────
|
# ── init command ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
disinto_init() {
|
disinto_init() {
|
||||||
|
|
@ -1065,6 +1216,9 @@ p.write_text(text)
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Provision Matrix homeserver (compose mode only)
|
||||||
|
setup_matrix
|
||||||
|
|
||||||
# Create labels on remote
|
# Create labels on remote
|
||||||
create_labels "$forge_repo" "$forge_url"
|
create_labels "$forge_repo" "$forge_url"
|
||||||
|
|
||||||
|
|
@ -1100,7 +1254,7 @@ p.write_text(text)
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Starting full stack ────────────────────────────────"
|
echo "── Starting full stack ────────────────────────────────"
|
||||||
docker compose -f "${FACTORY_ROOT}/docker-compose.yml" up -d
|
docker compose -f "${FACTORY_ROOT}/docker-compose.yml" up -d
|
||||||
echo "Stack: running (forgejo + woodpecker + agents)"
|
echo "Stack: running (forgejo + woodpecker + dendrite + agents)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,14 @@ log "Claude CLI: $(claude --version 2>&1 || true)"
|
||||||
|
|
||||||
install_project_crons
|
install_project_crons
|
||||||
|
|
||||||
|
# Start matrix listener in background (if configured)
|
||||||
|
if [ -n "${MATRIX_TOKEN:-}" ] && [ -n "${MATRIX_ROOM_ID:-}" ]; then
|
||||||
|
log "Starting matrix listener in background"
|
||||||
|
su -s /bin/bash agent -c "${DISINTO_DIR}/lib/matrix_listener.sh" &
|
||||||
|
else
|
||||||
|
log "Matrix listener: skipped (MATRIX_TOKEN or MATRIX_ROOM_ID not set)"
|
||||||
|
fi
|
||||||
|
|
||||||
# Run cron in the foreground. Cron jobs execute as the agent user.
|
# Run cron in the foreground. Cron jobs execute as the agent user.
|
||||||
log "Starting cron daemon"
|
log "Starting cron daemon"
|
||||||
exec cron -f
|
exec cron -f
|
||||||
|
|
|
||||||
12
lib/env.sh
12
lib/env.sh
|
|
@ -67,6 +67,18 @@ export WOODPECKER_REPO_ID="${WOODPECKER_REPO_ID:-}"
|
||||||
export WOODPECKER_SERVER="${WOODPECKER_SERVER:-http://localhost:8000}"
|
export WOODPECKER_SERVER="${WOODPECKER_SERVER:-http://localhost:8000}"
|
||||||
export CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-7200}"
|
export CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-7200}"
|
||||||
|
|
||||||
|
# Matrix homeserver: inside compose Dendrite is at http://dendrite:8008,
|
||||||
|
# on bare metal it defaults to http://localhost:8008.
|
||||||
|
if [ -z "${MATRIX_HOMESERVER:-}" ]; then
|
||||||
|
if [ "${DISINTO_CONTAINER:-}" = "1" ]; then
|
||||||
|
export MATRIX_HOMESERVER="http://dendrite:8008"
|
||||||
|
else
|
||||||
|
export MATRIX_HOMESERVER="http://localhost:8008"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
export MATRIX_HOMESERVER
|
||||||
|
fi
|
||||||
|
|
||||||
# Shared log helper
|
# Shared log helper
|
||||||
log() {
|
log() {
|
||||||
printf '[%s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*"
|
printf '[%s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
# Legacy systemd unit for bare-metal deployments (disinto init --bare).
|
||||||
|
# In compose mode, the matrix listener runs inside the agent container
|
||||||
|
# as a background process — see docker/agents/entrypoint.sh.
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Disinto Matrix Listener
|
Description=Disinto Matrix Listener
|
||||||
After=network.target dendrite.service
|
After=network.target dendrite.service
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,19 @@ set -euo pipefail
|
||||||
# Load shared environment
|
# Load shared environment
|
||||||
source "$(dirname "$0")/../lib/env.sh"
|
source "$(dirname "$0")/../lib/env.sh"
|
||||||
|
|
||||||
# Pidfile guard — prevent duplicate listener processes
|
# Pidfile guard — prevent duplicate listener processes.
|
||||||
|
# Inside a container the PID file from a previous run is stale (container
|
||||||
|
# restart resets the PID namespace), so we only honour it when the recorded
|
||||||
|
# PID is still alive.
|
||||||
PIDFILE="/tmp/matrix-listener.pid"
|
PIDFILE="/tmp/matrix-listener.pid"
|
||||||
if [ -f "$PIDFILE" ]; then
|
if [ -f "$PIDFILE" ]; then
|
||||||
OLD_PID=$(cat "$PIDFILE")
|
OLD_PID=$(cat "$PIDFILE" 2>/dev/null || true)
|
||||||
if kill -0 "$OLD_PID" 2>/dev/null; then
|
if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
|
||||||
echo "Listener already running (PID $OLD_PID)" >&2
|
echo "Listener already running (PID $OLD_PID)" >&2
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
# Stale pidfile (previous container run or crashed process) — remove it
|
||||||
|
rm -f "$PIDFILE"
|
||||||
fi
|
fi
|
||||||
echo $$ > "$PIDFILE"
|
echo $$ > "$PIDFILE"
|
||||||
trap 'rm -f "$PIDFILE"' EXIT
|
trap 'rm -f "$PIDFILE"' EXIT
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue