#!/usr/bin/env bash # ============================================================================= # generators — template generation functions for disinto init # # Generates docker-compose.yml, Dockerfile, Caddyfile, staging index, and # deployment pipeline configs. # # Globals expected (must be set before sourcing): # FACTORY_ROOT - Root of the disinto factory # PROJECT_NAME - Project name for the project repo (defaults to 'project') # PRIMARY_BRANCH - Primary branch name (defaults to 'main') # # Usage: # source "${FACTORY_ROOT}/lib/generators.sh" # generate_compose "$forge_port" # generate_caddyfile # generate_staging_index # generate_deploy_pipelines "$repo_root" "$project_name" # ============================================================================= set -euo pipefail # Assert required globals are set : "${FACTORY_ROOT:?FACTORY_ROOT must be set}" # PROJECT_NAME defaults to 'project' if not set (env.sh may have set it from FORGE_REPO) PROJECT_NAME="${PROJECT_NAME:-project}" # PRIMARY_BRANCH defaults to main (env.sh may have set it to 'master') PRIMARY_BRANCH="${PRIMARY_BRANCH:-main}" # Generate docker-compose.yml in the factory root. _generate_compose_impl() { local forge_port="${1:-3000}" local compose_file="${FACTORY_ROOT}/docker-compose.yml" # Check if compose file already exists if [ -f "$compose_file" ]; then echo "Compose: ${compose_file} (already exists, skipping)" return 0 fi cat > "$compose_file" <<'COMPOSEEOF' # docker-compose.yml — generated by disinto init # Brings up Forgejo, Woodpecker, and the agent runtime. services: forgejo: image: codeberg.org/forgejo/forgejo:11.0 restart: unless-stopped security_opt: - apparmor=unconfined volumes: - forgejo-data:/data environment: FORGEJO__database__DB_TYPE: sqlite3 FORGEJO__server__ROOT_URL: http://forgejo:3000/ FORGEJO__server__HTTP_PORT: "3000" FORGEJO__security__INSTALL_LOCK: "true" FORGEJO__service__DISABLE_REGISTRATION: "true" FORGEJO__webhook__ALLOWED_HOST_LIST: "private" networks: - disinto-net woodpecker: image: woodpeckerci/woodpecker-server:v3 restart: unless-stopped security_opt: - apparmor=unconfined ports: - "8000:8000" - "9000:9000" 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: ${WOODPECKER_HOST:-http://woodpecker:8000} WOODPECKER_OPEN: "true" WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET:-} WOODPECKER_DATABASE_DRIVER: sqlite3 WOODPECKER_DATABASE_DATASOURCE: /var/lib/woodpecker/woodpecker.sqlite depends_on: - forgejo networks: - disinto-net woodpecker-agent: image: woodpeckerci/woodpecker-agent:v3 restart: unless-stopped network_mode: host privileged: true volumes: - /var/run/docker.sock:/var/run/docker.sock environment: WOODPECKER_SERVER: localhost:9000 WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET:-} WOODPECKER_GRPC_SECURE: "false" WOODPECKER_HEALTHCHECK_ADDR: ":3333" WOODPECKER_BACKEND_DOCKER_NETWORK: disinto_disinto-net WOODPECKER_MAX_WORKFLOWS: 1 depends_on: - woodpecker agents: build: context: . dockerfile: docker/agents/Dockerfile restart: unless-stopped security_opt: - apparmor=unconfined volumes: - agent-data:/home/agent/data - project-repos:/home/agent/repos - ${HOME}/.claude:/home/agent/.claude - ${HOME}/.claude.json:/home/agent/.claude.json:ro - CLAUDE_BIN_PLACEHOLDER:/usr/local/bin/claude:ro - ${HOME}/.ssh:/home/agent/.ssh:ro - ${HOME}/.config/sops/age:/home/agent/.config/sops/age:ro - woodpecker-data:/woodpecker-data:ro environment: FORGE_URL: http://forgejo:3000 WOODPECKER_SERVER: http://woodpecker:8000 DISINTO_CONTAINER: "1" PROJECT_REPO_ROOT: /home/agent/repos/${PROJECT_NAME:-project} WOODPECKER_DATA_DIR: /woodpecker-data env_file: - .env # IMPORTANT: agents get .env only (forge tokens, CI tokens, config). # Vault-only secrets (GITHUB_TOKEN, CLAWHUB_TOKEN, deploy keys) live in # .env.vault.enc and are NEVER injected here — only the runner # container receives them at fire time (AD-006, #745). depends_on: - forgejo - woodpecker networks: - disinto-net runner: build: context: . dockerfile: docker/agents/Dockerfile profiles: ["vault"] security_opt: - apparmor=unconfined volumes: - agent-data:/home/agent/data environment: FORGE_URL: http://forgejo:3000 DISINTO_CONTAINER: "1" PROJECT_REPO_ROOT: /home/agent/repos/${PROJECT_NAME:-project} # Vault redesign in progress (PR-based approval, see #73-#77) # This container is being replaced — entrypoint will be updated in follow-up networks: - disinto-net # Edge proxy — reverse proxy to Forgejo, Woodpecker, and staging # Serves on ports 80/443, routes based on path edge: build: ./docker/edge ports: - "80:80" - "443:443" environment: - DISINTO_VERSION=${DISINTO_VERSION:-main} - FORGE_URL=http://forgejo:3000 - FORGE_REPO=${FORGE_REPO:-disinto-admin/disinto} - FORGE_OPS_REPO=${FORGE_OPS_REPO:-disinto-admin/disinto-ops} - FORGE_TOKEN=${FORGE_TOKEN:-} - FORGE_ADMIN_USERS=${FORGE_ADMIN_USERS:-disinto-admin} - FORGE_ADMIN_TOKEN=${FORGE_ADMIN_TOKEN:-} - OPS_REPO_ROOT=/opt/disinto-ops - PROJECT_REPO_ROOT=/opt/disinto - PRIMARY_BRANCH=main volumes: - ./docker/Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data - /var/run/docker.sock:/var/run/docker.sock depends_on: - forgejo - woodpecker - staging networks: - disinto-net # Staging container — static file server for staging artifacts # Edge proxy routes to this container for default requests staging: image: caddy:alpine command: ["caddy", "file-server", "--root", "/srv/site"] volumes: - ./docker:/srv/site:ro networks: - disinto-net # Staging deployment slot — activated by Woodpecker staging pipeline (#755). # Profile-gated: only starts when explicitly targeted by deploy commands. # Customize image/ports/volumes for your project after init. staging-deploy: image: alpine:3 profiles: ["staging"] security_opt: - apparmor=unconfined environment: DEPLOY_ENV: staging networks: - disinto-net command: ["echo", "staging slot — replace with project image"] volumes: forgejo-data: woodpecker-data: agent-data: project-repos: caddy_data: networks: disinto-net: driver: bridge COMPOSEEOF # Patch the Claude CLI binary path — resolve from host PATH at init time. local claude_bin claude_bin="$(command -v claude 2>/dev/null || true)" if [ -n "$claude_bin" ]; then # Resolve symlinks to get the real binary path claude_bin="$(readlink -f "$claude_bin")" sed -i "s|CLAUDE_BIN_PLACEHOLDER|${claude_bin}|" "$compose_file" else echo "Warning: claude CLI not found in PATH — update docker-compose.yml volumes manually" >&2 sed -i "s|CLAUDE_BIN_PLACEHOLDER|/usr/local/bin/claude|" "$compose_file" fi # 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: codeberg\.org\/forgejo\/forgejo:11\.0/a\\ ports:\\n - \"${forge_port}:3000\"" "$compose_file" else sed -i "/image: codeberg\.org\/forgejo\/forgejo:11\.0/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_impl() { 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 } # Generate docker/Caddyfile template for edge proxy. _generate_caddyfile_impl() { local docker_dir="${FACTORY_ROOT}/docker" local caddyfile="${docker_dir}/Caddyfile" if [ -f "$caddyfile" ]; then echo "Caddyfile: ${caddyfile} (already exists, skipping)" return fi cat > "$caddyfile" <<'CADDYFILEEOF' # Caddyfile — edge proxy configuration # IP-only binding at bootstrap; domain + TLS added later via vault resource request :80 { # Reverse proxy to Forgejo handle /forgejo/* { reverse_proxy forgejo:3000 } # Reverse proxy to Woodpecker CI handle /ci/* { reverse_proxy woodpecker:8000 } # Default: proxy to staging container handle { reverse_proxy staging:80 } } CADDYFILEEOF echo "Created: ${caddyfile}" } # Generate docker/index.html default page. _generate_staging_index_impl() { local docker_dir="${FACTORY_ROOT}/docker" local index_file="${docker_dir}/index.html" if [ -f "$index_file" ]; then echo "Staging: ${index_file} (already exists, skipping)" return fi cat > "$index_file" <<'INDEXEOF' Nothing shipped yet

Nothing shipped yet

CI pipelines will update this page with your staging artifacts.

INDEXEOF echo "Created: ${index_file}" } # Generate template .woodpecker/ deployment pipeline configs in a project repo. # Creates staging.yml and production.yml alongside the project's existing CI config. # These pipelines trigger on Woodpecker's deployment event with environment filters. _generate_deploy_pipelines_impl() { local repo_root="$1" local project_name="$2" : "${project_name// /}" # Silence SC2034 - variable used in heredoc local wp_dir="${repo_root}/.woodpecker" mkdir -p "$wp_dir" # Skip if deploy pipelines already exist if [ -f "${wp_dir}/staging.yml" ] && [ -f "${wp_dir}/production.yml" ]; then echo "Deploy: .woodpecker/{staging,production}.yml (already exist)" return fi if [ ! -f "${wp_dir}/staging.yml" ]; then cat > "${wp_dir}/staging.yml" <<'STAGINGEOF' # .woodpecker/staging.yml — Staging deployment pipeline # Triggered by runner via Woodpecker promote API. # Human approves promotion in vault → runner calls promote → this runs. when: event: deployment environment: staging steps: - name: deploy-staging image: docker:27 commands: - echo "Deploying to staging environment..." - echo "Pipeline ${CI_PIPELINE_NUMBER} promoted from CI #${CI_PIPELINE_PARENT}" # Pull the image built by CI and deploy to staging # Customize these commands for your project: # - docker compose -f docker-compose.yml --profile staging up -d - echo "Staging deployment complete" - name: verify-staging image: alpine:3 commands: - echo "Verifying staging deployment..." # Add health checks, smoke tests, or integration tests here: # - curl -sf http://staging:8080/health || exit 1 - echo "Staging verification complete" STAGINGEOF echo "Created: ${wp_dir}/staging.yml" fi if [ ! -f "${wp_dir}/production.yml" ]; then cat > "${wp_dir}/production.yml" <<'PRODUCTIONEOF' # .woodpecker/production.yml — Production deployment pipeline # Triggered by runner via Woodpecker promote API. # Human approves promotion in vault → runner calls promote → this runs. when: event: deployment environment: production steps: - name: deploy-production image: docker:27 commands: - echo "Deploying to production environment..." - echo "Pipeline ${CI_PIPELINE_NUMBER} promoted from staging" # Pull the verified image and deploy to production # Customize these commands for your project: # - docker compose -f docker-compose.yml up -d - echo "Production deployment complete" - name: verify-production image: alpine:3 commands: - echo "Verifying production deployment..." # Add production health checks here: # - curl -sf http://production:8080/health || exit 1 - echo "Production verification complete" PRODUCTIONEOF echo "Created: ${wp_dir}/production.yml" fi }