#!/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}" # Helper: extract woodpecker_repo_id from a project TOML file # Returns empty string if not found or file doesn't exist _get_woodpecker_repo_id() { local toml_file="$1" if [ -f "$toml_file" ]; then python3 -c " import sys, tomllib try: with open(sys.argv[1], 'rb') as f: cfg = tomllib.load(f) ci = cfg.get('ci', {}) wp_id = ci.get('woodpecker_repo_id', '0') print(wp_id) except: print('0') " "$toml_file" 2>/dev/null || echo "0" else echo "0" fi } # Find all project TOML files and extract the highest woodpecker_repo_id # (used for the main agents service which doesn't have a per-project TOML) _get_primary_woodpecker_repo_id() { local projects_dir="${FACTORY_ROOT}/projects" local max_id="0" for toml in "${projects_dir}"/*.toml; do [ -f "$toml" ] || continue local repo_id repo_id=$(_get_woodpecker_repo_id "$toml") if [ -n "$repo_id" ] && [ "$repo_id" != "0" ]; then # Use the first non-zero repo_id found (or highest if multiple) if [ "$repo_id" -gt "$max_id" ] 2>/dev/null; then max_id="$repo_id" fi fi done echo "$max_id" } # Parse project TOML for local-model agents and emit compose services. # Writes service definitions to stdout; caller handles insertion into compose file. _generate_local_model_services() { local compose_file="$1" local projects_dir="${FACTORY_ROOT}/projects" local temp_file temp_file=$(mktemp) local has_services=false local all_vols="" # Find all project TOML files and extract [agents.*] sections for toml in "${projects_dir}"/*.toml; do [ -f "$toml" ] || continue # Get woodpecker_repo_id for this project local wp_repo_id wp_repo_id=$(_get_woodpecker_repo_id "$toml") # Parse [agents.*] sections using Python - output YAML-compatible format while IFS='=' read -r key value; do case "$key" in NAME) service_name="$value" ;; BASE_URL) base_url="$value" ;; MODEL) model="$value" ;; ROLES) roles="$value" ;; API_KEY) api_key="$value" ;; FORGE_USER) forge_user="$value" ;; COMPACT_PCT) compact_pct="$value" ;; POLL_INTERVAL) poll_interval_val="$value" ;; ---) if [ -n "$service_name" ] && [ -n "$base_url" ]; then cat >> "$temp_file" </dev/null) done if [ "$has_services" = true ]; then # Insert the services before the volumes section local temp_compose temp_compose=$(mktemp) # Get everything before volumes: sed -n '1,/^volumes:/p' "$compose_file" | sed '$d' > "$temp_compose" # Add the services cat "$temp_file" >> "$temp_compose" # Add the volumes section and everything after sed -n '/^volumes:/,$p' "$compose_file" >> "$temp_compose" # Add local-model volumes to the volumes section if [ -n "$all_vols" ]; then # Find the volumes section and add the new volumes sed -i "/^volumes:/{n;:a;n;/^[a-z]/!{s/$/\n$all_vols/;b};ba}" "$temp_compose" fi mv "$temp_compose" "$compose_file" fi rm -f "$temp_file" } # 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 # Extract primary woodpecker_repo_id from project TOML files local wp_repo_id wp_repo_id=$(_get_primary_woodpecker_repo_id) cat > "$compose_file" <&2 sed -i "s|CLAUDE_BIN_PLACEHOLDER|/usr/local/bin/claude|g" "$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 }