fix: Containerize full stack with docker-compose (#618)
Add docker-compose.yml generation, agent Dockerfile, and new CLI commands (up/down/logs/shell) so the full stack runs containerized. The --bare flag preserves the current bare-metal setup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e58e60fe7c
commit
e38866ab61
4 changed files with 329 additions and 21 deletions
238
bin/disinto
238
bin/disinto
|
|
@ -4,12 +4,19 @@
|
||||||
#
|
#
|
||||||
# Commands:
|
# Commands:
|
||||||
# disinto init <repo-url> [options] Bootstrap a new project
|
# disinto init <repo-url> [options] Bootstrap a new project
|
||||||
|
# disinto up Start the full stack (docker compose)
|
||||||
|
# disinto down Stop the full stack
|
||||||
|
# disinto logs [service] Tail service logs
|
||||||
|
# disinto shell Shell into the agent container
|
||||||
# disinto status Show factory status
|
# disinto status Show factory status
|
||||||
# disinto secrets <edit|show|migrate> Manage encrypted secrets
|
# disinto secrets <edit|show|migrate> Manage encrypted secrets
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# disinto init https://github.com/user/repo
|
# disinto init https://github.com/user/repo
|
||||||
# disinto init user/repo --branch main --ci-id 3
|
# disinto init user/repo --branch main --ci-id 3
|
||||||
|
# disinto init user/repo --bare (bare-metal, no compose)
|
||||||
|
# disinto up
|
||||||
|
# disinto down
|
||||||
# disinto status
|
# disinto status
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -25,6 +32,10 @@ disinto — autonomous code factory CLI
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
disinto init <repo-url> [options] Bootstrap a new project
|
disinto init <repo-url> [options] Bootstrap a new project
|
||||||
|
disinto up Start the full stack (docker compose)
|
||||||
|
disinto down Stop the full stack
|
||||||
|
disinto logs [service] Tail service logs
|
||||||
|
disinto shell Shell into the agent container
|
||||||
disinto status Show factory status
|
disinto status Show factory status
|
||||||
disinto secrets <edit|show|migrate> Manage encrypted secrets (.env.enc)
|
disinto secrets <edit|show|migrate> Manage encrypted secrets (.env.enc)
|
||||||
|
|
||||||
|
|
@ -33,6 +44,7 @@ Init options:
|
||||||
--repo-root <path> Local clone path (default: ~/name)
|
--repo-root <path> Local clone path (default: ~/name)
|
||||||
--ci-id <n> Woodpecker CI repo ID (default: 0 = no CI)
|
--ci-id <n> Woodpecker CI repo ID (default: 0 = no CI)
|
||||||
--forge-url <url> Forge base URL (default: http://localhost:3000)
|
--forge-url <url> Forge base URL (default: http://localhost:3000)
|
||||||
|
--bare Skip compose generation (bare-metal setup)
|
||||||
--yes Skip confirmation prompts
|
--yes Skip confirmation prompts
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -136,15 +148,129 @@ write_secrets_encrypted() {
|
||||||
|
|
||||||
FORGEJO_DATA_DIR="${HOME}/.disinto/forgejo"
|
FORGEJO_DATA_DIR="${HOME}/.disinto/forgejo"
|
||||||
|
|
||||||
|
# Generate docker-compose.yml in the factory root.
|
||||||
|
generate_compose() {
|
||||||
|
local forge_port="${1:-3000}"
|
||||||
|
local compose_file="${FACTORY_ROOT}/docker-compose.yml"
|
||||||
|
|
||||||
|
cat > "$compose_file" <<'COMPOSEEOF'
|
||||||
|
# docker-compose.yml — generated by disinto init
|
||||||
|
# Brings up Forgejo, Woodpecker, and the agent runtime.
|
||||||
|
|
||||||
|
services:
|
||||||
|
forgejo:
|
||||||
|
image: forgejo/forgejo:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- forgejo-data:/data
|
||||||
|
environment:
|
||||||
|
FORGEJO__database__DB_TYPE: sqlite3
|
||||||
|
FORGEJO__server__ROOT_URL: http://forgejo:3000/
|
||||||
|
FORGEJO__server__HTTP_PORT: "3000"
|
||||||
|
FORGEJO__service__DISABLE_REGISTRATION: "true"
|
||||||
|
networks:
|
||||||
|
- disinto-net
|
||||||
|
|
||||||
|
woodpecker:
|
||||||
|
image: woodpeckerci/woodpecker-server:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- woodpecker-data:/var/lib/woodpecker
|
||||||
|
environment:
|
||||||
|
WOODPECKER_FORGEJO: "true"
|
||||||
|
WOODPECKER_FORGEJO_URL: http://forgejo:3000
|
||||||
|
WOODPECKER_FORGEJO_CLIENT: ${WP_FORGEJO_CLIENT:-}
|
||||||
|
WOODPECKER_FORGEJO_SECRET: ${WP_FORGEJO_SECRET:-}
|
||||||
|
WOODPECKER_HOST: http://woodpecker:8000
|
||||||
|
WOODPECKER_DATABASE_DRIVER: sqlite3
|
||||||
|
WOODPECKER_DATABASE_DATASOURCE: /var/lib/woodpecker/woodpecker.sqlite
|
||||||
|
depends_on:
|
||||||
|
- forgejo
|
||||||
|
networks:
|
||||||
|
- disinto-net
|
||||||
|
|
||||||
|
agents:
|
||||||
|
build: ./docker/agents
|
||||||
|
restart: unless-stopped
|
||||||
|
user: "1000:1000"
|
||||||
|
volumes:
|
||||||
|
- agent-data:/home/agent/data
|
||||||
|
- project-repos:/home/agent/repos
|
||||||
|
- ./:/home/agent/disinto:ro
|
||||||
|
- claude-auth:/home/agent/.claude:ro
|
||||||
|
environment:
|
||||||
|
FORGE_URL: http://forgejo:3000
|
||||||
|
WOODPECKER_SERVER: http://woodpecker:8000
|
||||||
|
DISINTO_CONTAINER: "1"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
- forgejo
|
||||||
|
- woodpecker
|
||||||
|
networks:
|
||||||
|
- disinto-net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
forgejo-data:
|
||||||
|
woodpecker-data:
|
||||||
|
agent-data:
|
||||||
|
project-repos:
|
||||||
|
claude-auth:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
disinto-net:
|
||||||
|
driver: bridge
|
||||||
|
COMPOSEEOF
|
||||||
|
|
||||||
|
# Patch the forgejo port mapping into the file if non-default
|
||||||
|
if [ "$forge_port" != "3000" ]; then
|
||||||
|
# Add port mapping to forgejo service so it's reachable from host during init
|
||||||
|
sed -i "/image: forgejo\/forgejo:latest/a\\ ports:\\n - \"${forge_port}:3000\"" "$compose_file"
|
||||||
|
else
|
||||||
|
sed -i "/image: forgejo\/forgejo:latest/a\\ ports:\\n - \"3000:3000\"" "$compose_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Created: ${compose_file}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate docker/agents/ files if they don't already exist.
|
||||||
|
generate_agent_docker() {
|
||||||
|
local docker_dir="${FACTORY_ROOT}/docker/agents"
|
||||||
|
mkdir -p "$docker_dir"
|
||||||
|
|
||||||
|
if [ ! -f "${docker_dir}/Dockerfile" ]; then
|
||||||
|
echo "Warning: docker/agents/Dockerfile not found — expected in repo" >&2
|
||||||
|
fi
|
||||||
|
if [ ! -f "${docker_dir}/entrypoint.sh" ]; then
|
||||||
|
echo "Warning: docker/agents/entrypoint.sh not found — expected in repo" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check whether compose mode is active (docker-compose.yml exists).
|
||||||
|
is_compose_mode() {
|
||||||
|
[ -f "${FACTORY_ROOT}/docker-compose.yml" ]
|
||||||
|
}
|
||||||
|
|
||||||
# Provision or connect to a local Forgejo instance.
|
# Provision or connect to a local Forgejo instance.
|
||||||
# Creates admin + bot users, generates API tokens, stores in .env.
|
# Creates admin + bot users, generates API tokens, stores in .env.
|
||||||
|
# When $DISINTO_BARE is set, uses standalone docker run; otherwise uses compose.
|
||||||
setup_forge() {
|
setup_forge() {
|
||||||
local forge_url="$1"
|
local forge_url="$1"
|
||||||
local repo_slug="$2"
|
local repo_slug="$2"
|
||||||
|
local use_bare="${DISINTO_BARE:-false}"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Forge setup ────────────────────────────────────────"
|
echo "── Forge setup ────────────────────────────────────────"
|
||||||
|
|
||||||
|
# Helper: run a command inside the Forgejo container
|
||||||
|
_forgejo_exec() {
|
||||||
|
if [ "$use_bare" = true ]; then
|
||||||
|
docker exec disinto-forgejo "$@"
|
||||||
|
else
|
||||||
|
docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T forgejo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Check if Forgejo is already running
|
# Check if Forgejo is already running
|
||||||
if curl -sf --max-time 5 "${forge_url}/api/v1/version" >/dev/null 2>&1; then
|
if curl -sf --max-time 5 "${forge_url}/api/v1/version" >/dev/null 2>&1; then
|
||||||
echo "Forgejo: ${forge_url} (already running)"
|
echo "Forgejo: ${forge_url} (already running)"
|
||||||
|
|
@ -158,15 +284,15 @@ setup_forge() {
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create data directory
|
|
||||||
mkdir -p "${FORGEJO_DATA_DIR}"
|
|
||||||
|
|
||||||
# Extract port from forge_url
|
# Extract port from forge_url
|
||||||
local forge_port
|
local forge_port
|
||||||
forge_port=$(printf '%s' "$forge_url" | sed -E 's|.*:([0-9]+)/?$|\1|')
|
forge_port=$(printf '%s' "$forge_url" | sed -E 's|.*:([0-9]+)/?$|\1|')
|
||||||
forge_port="${forge_port:-3000}"
|
forge_port="${forge_port:-3000}"
|
||||||
|
|
||||||
# Start Forgejo container
|
if [ "$use_bare" = true ]; then
|
||||||
|
# Bare-metal mode: standalone docker run
|
||||||
|
mkdir -p "${FORGEJO_DATA_DIR}"
|
||||||
|
|
||||||
if docker ps -a --format '{{.Names}}' | grep -q '^disinto-forgejo$'; then
|
if docker ps -a --format '{{.Names}}' | grep -q '^disinto-forgejo$'; then
|
||||||
docker start disinto-forgejo >/dev/null 2>&1 || true
|
docker start disinto-forgejo >/dev/null 2>&1 || true
|
||||||
else
|
else
|
||||||
|
|
@ -182,6 +308,10 @@ setup_forge() {
|
||||||
-e "FORGEJO__service__DISABLE_REGISTRATION=true" \
|
-e "FORGEJO__service__DISABLE_REGISTRATION=true" \
|
||||||
forgejo/forgejo:latest
|
forgejo/forgejo:latest
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
# Compose mode: start Forgejo via docker compose
|
||||||
|
docker compose -f "${FACTORY_ROOT}/docker-compose.yml" up -d forgejo
|
||||||
|
fi
|
||||||
|
|
||||||
# Wait for Forgejo to become healthy
|
# Wait for Forgejo to become healthy
|
||||||
echo -n "Waiting for Forgejo to start"
|
echo -n "Waiting for Forgejo to start"
|
||||||
|
|
@ -206,7 +336,7 @@ setup_forge() {
|
||||||
|
|
||||||
if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${admin_user}" >/dev/null 2>&1; then
|
if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${admin_user}" >/dev/null 2>&1; then
|
||||||
echo "Creating admin user: ${admin_user}"
|
echo "Creating admin user: ${admin_user}"
|
||||||
docker exec disinto-forgejo forgejo admin user create \
|
_forgejo_exec forgejo admin user create \
|
||||||
--admin \
|
--admin \
|
||||||
--username "${admin_user}" \
|
--username "${admin_user}" \
|
||||||
--password "${admin_pass}" \
|
--password "${admin_pass}" \
|
||||||
|
|
@ -241,7 +371,7 @@ setup_forge() {
|
||||||
-H "Authorization: token ${admin_token}" \
|
-H "Authorization: token ${admin_token}" \
|
||||||
"${forge_url}/api/v1/users/${bot_user}" >/dev/null 2>&1; then
|
"${forge_url}/api/v1/users/${bot_user}" >/dev/null 2>&1; then
|
||||||
echo "Creating bot user: ${bot_user}"
|
echo "Creating bot user: ${bot_user}"
|
||||||
docker exec disinto-forgejo forgejo admin user create \
|
_forgejo_exec forgejo admin user create \
|
||||||
--username "${bot_user}" \
|
--username "${bot_user}" \
|
||||||
--password "${bot_pass}" \
|
--password "${bot_pass}" \
|
||||||
--email "${bot_user}@disinto.local" \
|
--email "${bot_user}@disinto.local" \
|
||||||
|
|
@ -748,18 +878,22 @@ disinto_init() {
|
||||||
shift
|
shift
|
||||||
|
|
||||||
# Parse flags
|
# Parse flags
|
||||||
local branch="" repo_root="" ci_id="0" auto_yes=false forge_url_flag=""
|
local branch="" repo_root="" ci_id="0" auto_yes=false forge_url_flag="" bare=false
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--branch) branch="$2"; shift 2 ;;
|
--branch) branch="$2"; shift 2 ;;
|
||||||
--repo-root) repo_root="$2"; shift 2 ;;
|
--repo-root) repo_root="$2"; shift 2 ;;
|
||||||
--ci-id) ci_id="$2"; shift 2 ;;
|
--ci-id) ci_id="$2"; shift 2 ;;
|
||||||
--forge-url) forge_url_flag="$2"; shift 2 ;;
|
--forge-url) forge_url_flag="$2"; shift 2 ;;
|
||||||
|
--bare) bare=true; shift ;;
|
||||||
--yes) auto_yes=true; shift ;;
|
--yes) auto_yes=true; shift ;;
|
||||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Export bare-metal flag for setup_forge
|
||||||
|
export DISINTO_BARE="$bare"
|
||||||
|
|
||||||
# Extract org/repo slug
|
# Extract org/repo slug
|
||||||
local forge_repo
|
local forge_repo
|
||||||
forge_repo=$(parse_repo_slug "$repo_url")
|
forge_repo=$(parse_repo_slug "$repo_url")
|
||||||
|
|
@ -829,6 +963,15 @@ p.write_text(text)
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Generate compose files (unless --bare)
|
||||||
|
if [ "$bare" = false ]; then
|
||||||
|
local forge_port
|
||||||
|
forge_port=$(printf '%s' "$forge_url" | sed -E 's|.*:([0-9]+)/?$|\1|')
|
||||||
|
forge_port="${forge_port:-3000}"
|
||||||
|
generate_compose "$forge_port"
|
||||||
|
generate_agent_docker
|
||||||
|
fi
|
||||||
|
|
||||||
# Set up local Forgejo instance (provision if needed, create users/tokens/repo)
|
# Set up local Forgejo instance (provision if needed, create users/tokens/repo)
|
||||||
setup_forge "$forge_url" "$forge_repo"
|
setup_forge "$forge_url" "$forge_repo"
|
||||||
|
|
||||||
|
|
@ -919,11 +1062,24 @@ p.write_text(text)
|
||||||
# Encrypt secrets if SOPS + age are available
|
# Encrypt secrets if SOPS + age are available
|
||||||
write_secrets_encrypted
|
write_secrets_encrypted
|
||||||
|
|
||||||
|
# Bring up the full stack (compose mode only)
|
||||||
|
if [ "$bare" = false ] && [ -f "${FACTORY_ROOT}/docker-compose.yml" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "── Starting full stack ────────────────────────────────"
|
||||||
|
docker compose -f "${FACTORY_ROOT}/docker-compose.yml" up -d
|
||||||
|
echo "Stack: running (forgejo + woodpecker + agents)"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Done. Project ${project_name} is ready."
|
echo "Done. Project ${project_name} is ready."
|
||||||
echo " Config: ${toml_path}"
|
echo " Config: ${toml_path}"
|
||||||
echo " Clone: ${repo_root}"
|
echo " Clone: ${repo_root}"
|
||||||
echo " Forge: ${forge_url}/${forge_repo}"
|
echo " Forge: ${forge_url}/${forge_repo}"
|
||||||
|
if [ "$bare" = false ]; then
|
||||||
|
echo " Stack: docker compose (use 'disinto up/down/logs/shell')"
|
||||||
|
else
|
||||||
|
echo " Mode: bare-metal"
|
||||||
|
fi
|
||||||
echo " Run 'disinto status' to verify."
|
echo " Run 'disinto status' to verify."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1054,10 +1210,78 @@ disinto_secrets() {
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── up command ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
disinto_up() {
|
||||||
|
local compose_file="${FACTORY_ROOT}/docker-compose.yml"
|
||||||
|
if [ ! -f "$compose_file" ]; then
|
||||||
|
echo "Error: docker-compose.yml not found" >&2
|
||||||
|
echo " Run 'disinto init <repo-url>' first (without --bare)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Decrypt secrets to temp .env if SOPS available and .env.enc exists
|
||||||
|
local tmp_env=""
|
||||||
|
local enc_file="${FACTORY_ROOT}/.env.enc"
|
||||||
|
local env_file="${FACTORY_ROOT}/.env"
|
||||||
|
if [ -f "$enc_file" ] && command -v sops &>/dev/null && [ ! -f "$env_file" ]; then
|
||||||
|
tmp_env="${env_file}"
|
||||||
|
sops -d --output-type dotenv "$enc_file" > "$tmp_env"
|
||||||
|
echo "Decrypted secrets for compose"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker compose -f "$compose_file" up -d "$@"
|
||||||
|
echo "Stack is up"
|
||||||
|
|
||||||
|
# Clean up temp .env
|
||||||
|
if [ -n "$tmp_env" ] && [ -f "$tmp_env" ]; then
|
||||||
|
rm -f "$tmp_env"
|
||||||
|
echo "Removed temporary .env"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── down command ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
disinto_down() {
|
||||||
|
local compose_file="${FACTORY_ROOT}/docker-compose.yml"
|
||||||
|
if [ ! -f "$compose_file" ]; then
|
||||||
|
echo "Error: docker-compose.yml not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
docker compose -f "$compose_file" down "$@"
|
||||||
|
echo "Stack is down"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── logs command ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
disinto_logs() {
|
||||||
|
local compose_file="${FACTORY_ROOT}/docker-compose.yml"
|
||||||
|
if [ ! -f "$compose_file" ]; then
|
||||||
|
echo "Error: docker-compose.yml not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
docker compose -f "$compose_file" logs -f "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── shell command ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
disinto_shell() {
|
||||||
|
local compose_file="${FACTORY_ROOT}/docker-compose.yml"
|
||||||
|
if [ ! -f "$compose_file" ]; then
|
||||||
|
echo "Error: docker-compose.yml not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
docker compose -f "$compose_file" exec agents bash
|
||||||
|
}
|
||||||
|
|
||||||
# ── Main dispatch ────────────────────────────────────────────────────────────
|
# ── Main dispatch ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
init) shift; disinto_init "$@" ;;
|
init) shift; disinto_init "$@" ;;
|
||||||
|
up) shift; disinto_up "$@" ;;
|
||||||
|
down) shift; disinto_down "$@" ;;
|
||||||
|
logs) shift; disinto_logs "$@" ;;
|
||||||
|
shell) shift; disinto_shell ;;
|
||||||
status) shift; disinto_status "$@" ;;
|
status) shift; disinto_status "$@" ;;
|
||||||
secrets) shift; disinto_secrets "$@" ;;
|
secrets) shift; disinto_secrets "$@" ;;
|
||||||
-h|--help) usage ;;
|
-h|--help) usage ;;
|
||||||
|
|
|
||||||
20
docker/agents/Dockerfile
Normal file
20
docker/agents/Dockerfile
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
bash curl git jq tmux cron python3 openssh-client ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Claude CLI
|
||||||
|
RUN curl -fsSL https://cli.anthropic.com/install.sh | sh \
|
||||||
|
&& mv /root/.claude/local/claude /usr/local/bin/claude || true
|
||||||
|
|
||||||
|
# Non-root user
|
||||||
|
RUN useradd -m -u 1000 -s /bin/bash agent
|
||||||
|
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
USER agent
|
||||||
|
WORKDIR /home/agent
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
51
docker/agents/entrypoint.sh
Normal file
51
docker/agents/entrypoint.sh
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# entrypoint.sh — Start agent container with cron and stay alive
|
||||||
|
#
|
||||||
|
# Installs crontab entries from project TOMLs found in the factory
|
||||||
|
# mount, then runs cron in the background and tails the log.
|
||||||
|
|
||||||
|
DISINTO_DIR="${HOME}/disinto"
|
||||||
|
LOGFILE="${HOME}/data/agent-entrypoint.log"
|
||||||
|
mkdir -p "${HOME}/data"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[%s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" | tee -a "$LOGFILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build crontab from project TOMLs
|
||||||
|
install_project_crons() {
|
||||||
|
local cron_lines=""
|
||||||
|
for toml in "${DISINTO_DIR}"/projects/*.toml; do
|
||||||
|
[ -f "$toml" ] || continue
|
||||||
|
local pname
|
||||||
|
pname=$(python3 -c "
|
||||||
|
import sys, tomllib
|
||||||
|
with open(sys.argv[1], 'rb') as f:
|
||||||
|
print(tomllib.load(f)['name'])
|
||||||
|
" "$toml" 2>/dev/null) || continue
|
||||||
|
|
||||||
|
cron_lines="${cron_lines}
|
||||||
|
# disinto: ${pname}
|
||||||
|
2,7,12,17,22,27,32,37,42,47,52,57 * * * * ${DISINTO_DIR}/review/review-poll.sh ${toml} >/dev/null 2>&1
|
||||||
|
4,9,14,19,24,29,34,39,44,49,54,59 * * * * ${DISINTO_DIR}/dev/dev-poll.sh ${toml} >/dev/null 2>&1
|
||||||
|
0 0,6,12,18 * * * cd ${DISINTO_DIR} && bash gardener/gardener-run.sh ${toml} >/dev/null 2>&1"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$cron_lines" ]; then
|
||||||
|
printf '%s\n' "$cron_lines" | crontab -
|
||||||
|
log "Installed crontab for projects"
|
||||||
|
else
|
||||||
|
log "No project TOMLs found — crontab empty"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log "Agent container starting"
|
||||||
|
install_project_crons
|
||||||
|
|
||||||
|
# Keep container alive — cron runs in foreground via tail on log
|
||||||
|
# (cron daemon needs root; since we run as agent, we use a polling approach
|
||||||
|
# or the host cron can be used via docker compose exec)
|
||||||
|
log "Agent container ready — waiting for work"
|
||||||
|
exec tail -f /dev/null
|
||||||
13
lib/env.sh
13
lib/env.sh
|
|
@ -7,6 +7,14 @@ set -euo pipefail
|
||||||
# Resolve script root (parent of lib/)
|
# Resolve script root (parent of lib/)
|
||||||
FACTORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
FACTORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
# Container detection: when running inside the agent container, DISINTO_CONTAINER
|
||||||
|
# is set by docker-compose.yml. Adjust paths so phase files, logs, and thread
|
||||||
|
# maps land on the persistent volume instead of /tmp (which is ephemeral).
|
||||||
|
if [ "${DISINTO_CONTAINER:-}" = "1" ]; then
|
||||||
|
DISINTO_DATA_DIR="${HOME}/data"
|
||||||
|
mkdir -p "${DISINTO_DATA_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Load secrets: prefer .env.enc (SOPS-encrypted), fall back to plaintext .env
|
# Load secrets: prefer .env.enc (SOPS-encrypted), fall back to plaintext .env
|
||||||
if [ -f "$FACTORY_ROOT/.env.enc" ] && command -v sops &>/dev/null; then
|
if [ -f "$FACTORY_ROOT/.env.enc" ] && command -v sops &>/dev/null; then
|
||||||
set -a
|
set -a
|
||||||
|
|
@ -123,7 +131,12 @@ wpdb() {
|
||||||
# Matrix messaging helper — usage: matrix_send <prefix> <message> [thread_event_id] [context_tag]
|
# Matrix messaging helper — usage: matrix_send <prefix> <message> [thread_event_id] [context_tag]
|
||||||
# Returns event_id on stdout. Registers threads for listener dispatch.
|
# Returns event_id on stdout. Registers threads for listener dispatch.
|
||||||
# context_tag is stored in the thread map (e.g. issue number) for routing replies.
|
# context_tag is stored in the thread map (e.g. issue number) for routing replies.
|
||||||
|
# Thread map: use persistent data dir inside container, /tmp on bare metal
|
||||||
|
if [ "${DISINTO_CONTAINER:-}" = "1" ]; then
|
||||||
|
MATRIX_THREAD_MAP="${MATRIX_THREAD_MAP:-${DISINTO_DATA_DIR}/matrix-thread-map}"
|
||||||
|
else
|
||||||
MATRIX_THREAD_MAP="${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}"
|
MATRIX_THREAD_MAP="${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}"
|
||||||
|
fi
|
||||||
matrix_send() {
|
matrix_send() {
|
||||||
[ -z "${MATRIX_TOKEN:-}" ] && return 0
|
[ -z "${MATRIX_TOKEN:-}" ] && return 0
|
||||||
local prefix="$1" msg="$2" thread_id="${3:-}" ctx_tag="${4:-}"
|
local prefix="$1" msg="$2" thread_id="${3:-}" ctx_tag="${4:-}"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue