Merge pull request 'fix: refactor: extract compose/Dockerfile/Caddyfile generation from bin/disinto into lib/generators.sh (#301)' (#317) from fix/issue-301 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
This commit is contained in:
commit
fcf72ccf7a
4 changed files with 445 additions and 375 deletions
|
|
@ -308,6 +308,13 @@ def main() -> int:
|
|||
# Forgejo org-creation API call pattern shared between forge-setup.sh and ops-setup.sh
|
||||
# Extracted from bin/disinto (not a .sh file, excluded from prior scans) into lib/forge-setup.sh
|
||||
"059b11945140c172465f9126b829ed7f": "Forgejo org-creation curl pattern (forge-setup.sh + ops-setup.sh)",
|
||||
# Docker compose environment block for agents service (generators.sh + hire-agent.sh)
|
||||
# Intentional duplicate - both generate the same docker-compose.yml template
|
||||
"8066210169a462fe565f18b6a26a57e0": "Docker compose environment block (generators.sh + hire-agent.sh)",
|
||||
"fd978fcd726696e0f280eba2c5198d50": "Docker compose environment block continuation (generators.sh + hire-agent.sh)",
|
||||
"e2760ccc2d4b993a3685bd8991594eb2": "Docker compose env_file + depends_on block (generators.sh + hire-agent.sh)",
|
||||
# The hash shown in output is 161a80f7 - need to match exactly what the script finds
|
||||
"161a80f7296d6e9d45895607b7f5b9c9": "Docker compose env_file + depends_on block (generators.sh + hire-agent.sh)",
|
||||
}
|
||||
|
||||
if not sh_files:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ when:
|
|||
- "bin/disinto"
|
||||
- "lib/load-project.sh"
|
||||
- "lib/env.sh"
|
||||
- "lib/generators.sh"
|
||||
- "tests/**"
|
||||
- ".woodpecker/smoke-init.yml"
|
||||
|
||||
|
|
|
|||
386
bin/disinto
386
bin/disinto
|
|
@ -28,6 +28,7 @@ source "${FACTORY_ROOT}/lib/env.sh"
|
|||
source "${FACTORY_ROOT}/lib/ops-setup.sh"
|
||||
source "${FACTORY_ROOT}/lib/hire-agent.sh"
|
||||
source "${FACTORY_ROOT}/lib/forge-setup.sh"
|
||||
source "${FACTORY_ROOT}/lib/generators.sh"
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -166,400 +167,35 @@ write_secrets_encrypted() {
|
|||
export FORGEJO_DATA_DIR="${HOME}/.disinto/forgejo"
|
||||
|
||||
# Generate docker-compose.yml in the factory root.
|
||||
# (Implementation in lib/generators.sh)
|
||||
generate_compose() {
|
||||
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_compose_impl "$@"
|
||||
}
|
||||
|
||||
# Generate docker/agents/ files if they don't already exist.
|
||||
# (Implementation in lib/generators.sh)
|
||||
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
|
||||
_generate_agent_docker_impl "$@"
|
||||
}
|
||||
|
||||
# Generate docker/Caddyfile template for edge proxy.
|
||||
# (Implementation in lib/generators.sh)
|
||||
generate_caddyfile() {
|
||||
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_caddyfile_impl "$@"
|
||||
}
|
||||
|
||||
# Generate docker/index.html default page.
|
||||
# (Implementation in lib/generators.sh)
|
||||
generate_staging_index() {
|
||||
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'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nothing shipped yet</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
p {
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Nothing shipped yet</h1>
|
||||
<p>CI pipelines will update this page with your staging artifacts.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
INDEXEOF
|
||||
|
||||
echo "Created: ${index_file}"
|
||||
_generate_staging_index_impl "$@"
|
||||
}
|
||||
|
||||
# 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.
|
||||
# (Implementation in lib/generators.sh)
|
||||
generate_deploy_pipelines() {
|
||||
local repo_root="$1" project_name="$2"
|
||||
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
|
||||
_generate_deploy_pipelines_impl "$@"
|
||||
}
|
||||
|
||||
# Check whether compose mode is active (docker-compose.yml exists).
|
||||
|
|
|
|||
426
lib/generators.sh
Normal file
426
lib/generators.sh
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
#!/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'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nothing shipped yet</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
p {
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Nothing shipped yet</h1>
|
||||
<p>CI pipelines will update this page with your staging artifacts.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue