Compare commits
4 commits
cbc28d34f3
...
c62f8e77ef
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c62f8e77ef | ||
| e65e091d3c | |||
|
|
c7e7fd00ea | ||
|
|
8c42303943 |
6 changed files with 938 additions and 842 deletions
|
|
@ -305,6 +305,9 @@ def main() -> int:
|
||||||
# Structural end-of-while-loop+case pattern: `return 1 ;; esac done }`
|
# Structural end-of-while-loop+case pattern: `return 1 ;; esac done }`
|
||||||
# Appears in stack_lock_acquire (lib/stack-lock.sh) and lib/pr-lifecycle.sh
|
# Appears in stack_lock_acquire (lib/stack-lock.sh) and lib/pr-lifecycle.sh
|
||||||
"29d4f34b703f44699237713cc8d8065b": "Structural end-of-while-loop+case (return 1, esac, done, closing brace)",
|
"29d4f34b703f44699237713cc8d8065b": "Structural end-of-while-loop+case (return 1, esac, done, closing brace)",
|
||||||
|
# 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)",
|
||||||
}
|
}
|
||||||
|
|
||||||
if not sh_files:
|
if not sh_files:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ when:
|
||||||
- "bin/disinto"
|
- "bin/disinto"
|
||||||
- "lib/load-project.sh"
|
- "lib/load-project.sh"
|
||||||
- "lib/env.sh"
|
- "lib/env.sh"
|
||||||
|
- "lib/generators.sh"
|
||||||
- "tests/**"
|
- "tests/**"
|
||||||
- ".woodpecker/smoke-init.yml"
|
- ".woodpecker/smoke-init.yml"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ disinto/ (code repo)
|
||||||
│ supervisor-poll.sh — legacy bash orchestrator (superseded)
|
│ supervisor-poll.sh — legacy bash orchestrator (superseded)
|
||||||
├── architect/ architect-run.sh — strategic decomposition of vision into sprints
|
├── architect/ architect-run.sh — strategic decomposition of vision into sprints
|
||||||
├── vault/ vault-env.sh — shared env setup (vault redesign in progress, see #73-#77)
|
├── vault/ vault-env.sh — shared env setup (vault redesign in progress, see #73-#77)
|
||||||
├── lib/ env.sh, agent-sdk.sh, ci-helpers.sh, ci-debug.sh, load-project.sh, parse-deps.sh, guard.sh, mirrors.sh, pr-lifecycle.sh, issue-lifecycle.sh, worktree.sh, formula-session.sh, stack-lock.sh, build-graph.py
|
├── lib/ env.sh, agent-sdk.sh, ci-helpers.sh, ci-debug.sh, load-project.sh, parse-deps.sh, guard.sh, mirrors.sh, pr-lifecycle.sh, issue-lifecycle.sh, worktree.sh, formula-session.sh, stack-lock.sh, forge-setup.sh, build-graph.py
|
||||||
├── projects/ *.toml.example — templates; *.toml — local per-box config (gitignored)
|
├── projects/ *.toml.example — templates; *.toml — local per-box config (gitignored)
|
||||||
├── formulas/ Issue templates (TOML specs for multi-step agent tasks)
|
├── formulas/ Issue templates (TOML specs for multi-step agent tasks)
|
||||||
└── docs/ Protocol docs (PHASE-PROTOCOL.md, EVIDENCE-ARCHITECTURE.md)
|
└── docs/ Protocol docs (PHASE-PROTOCOL.md, EVIDENCE-ARCHITECTURE.md)
|
||||||
|
|
|
||||||
854
bin/disinto
854
bin/disinto
|
|
@ -27,19 +27,11 @@ FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
source "${FACTORY_ROOT}/lib/env.sh"
|
source "${FACTORY_ROOT}/lib/env.sh"
|
||||||
source "${FACTORY_ROOT}/lib/ops-setup.sh"
|
source "${FACTORY_ROOT}/lib/ops-setup.sh"
|
||||||
source "${FACTORY_ROOT}/lib/hire-agent.sh"
|
source "${FACTORY_ROOT}/lib/hire-agent.sh"
|
||||||
|
source "${FACTORY_ROOT}/lib/forge-setup.sh"
|
||||||
|
source "${FACTORY_ROOT}/lib/generators.sh"
|
||||||
|
|
||||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Execute a command in the Forgejo container (for admin operations)
|
|
||||||
_forgejo_exec() {
|
|
||||||
local use_bare="${DISINTO_BARE:-false}"
|
|
||||||
if [ "$use_bare" = true ]; then
|
|
||||||
docker exec -u git disinto-forgejo "$@"
|
|
||||||
else
|
|
||||||
docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T -u git forgejo "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
disinto — autonomous code factory CLI
|
disinto — autonomous code factory CLI
|
||||||
|
|
@ -172,403 +164,38 @@ write_secrets_encrypted() {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
FORGEJO_DATA_DIR="${HOME}/.disinto/forgejo"
|
export FORGEJO_DATA_DIR="${HOME}/.disinto/forgejo"
|
||||||
|
|
||||||
# Generate docker-compose.yml in the factory root.
|
# Generate docker-compose.yml in the factory root.
|
||||||
|
# (Implementation in lib/generators.sh)
|
||||||
generate_compose() {
|
generate_compose() {
|
||||||
local forge_port="${1:-3000}"
|
_generate_compose_impl "$@"
|
||||||
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 docker/agents/ files if they don't already exist.
|
||||||
|
# (Implementation in lib/generators.sh)
|
||||||
generate_agent_docker() {
|
generate_agent_docker() {
|
||||||
local docker_dir="${FACTORY_ROOT}/docker/agents"
|
_generate_agent_docker_impl "$@"
|
||||||
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 docker/Caddyfile template for edge proxy.
|
||||||
|
# (Implementation in lib/generators.sh)
|
||||||
generate_caddyfile() {
|
generate_caddyfile() {
|
||||||
local docker_dir="${FACTORY_ROOT}/docker"
|
_generate_caddyfile_impl "$@"
|
||||||
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 docker/index.html default page.
|
||||||
|
# (Implementation in lib/generators.sh)
|
||||||
generate_staging_index() {
|
generate_staging_index() {
|
||||||
local docker_dir="${FACTORY_ROOT}/docker"
|
_generate_staging_index_impl "$@"
|
||||||
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.
|
# Generate template .woodpecker/ deployment pipeline configs in a project repo.
|
||||||
# Creates staging.yml and production.yml alongside the project's existing CI config.
|
# Creates staging.yml and production.yml alongside the project's existing CI config.
|
||||||
# These pipelines trigger on Woodpecker's deployment event with environment filters.
|
# These pipelines trigger on Woodpecker's deployment event with environment filters.
|
||||||
|
# (Implementation in lib/generators.sh)
|
||||||
generate_deploy_pipelines() {
|
generate_deploy_pipelines() {
|
||||||
local repo_root="$1" project_name="$2"
|
_generate_deploy_pipelines_impl "$@"
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check whether compose mode is active (docker-compose.yml exists).
|
# Check whether compose mode is active (docker-compose.yml exists).
|
||||||
|
|
@ -576,461 +203,6 @@ is_compose_mode() {
|
||||||
[ -f "${FACTORY_ROOT}/docker-compose.yml" ]
|
[ -f "${FACTORY_ROOT}/docker-compose.yml" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Provision or connect to a local Forgejo instance.
|
|
||||||
# Creates admin + bot users, generates API tokens, stores in .env.
|
|
||||||
# When $DISINTO_BARE is set, uses standalone docker run; otherwise uses compose.
|
|
||||||
setup_forge() {
|
|
||||||
local forge_url="$1"
|
|
||||||
local repo_slug="$2"
|
|
||||||
local use_bare="${DISINTO_BARE:-false}"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "── Forge setup ────────────────────────────────────────"
|
|
||||||
|
|
||||||
# Check if Forgejo is already running
|
|
||||||
if curl -sf --max-time 5 "${forge_url}/api/v1/version" >/dev/null 2>&1; then
|
|
||||||
echo "Forgejo: ${forge_url} (already running)"
|
|
||||||
else
|
|
||||||
echo "Forgejo not reachable at ${forge_url}"
|
|
||||||
echo "Starting Forgejo via Docker..."
|
|
||||||
|
|
||||||
if ! command -v docker &>/dev/null; then
|
|
||||||
echo "Error: docker not found — needed to provision Forgejo" >&2
|
|
||||||
echo " Install Docker or start Forgejo manually at ${forge_url}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract port from forge_url
|
|
||||||
local forge_port
|
|
||||||
forge_port=$(printf '%s' "$forge_url" | sed -E 's|.*:([0-9]+)/?$|\1|')
|
|
||||||
forge_port="${forge_port:-3000}"
|
|
||||||
|
|
||||||
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
|
|
||||||
docker start disinto-forgejo >/dev/null 2>&1 || true
|
|
||||||
else
|
|
||||||
docker run -d \
|
|
||||||
--name disinto-forgejo \
|
|
||||||
--restart unless-stopped \
|
|
||||||
-p "${forge_port}:3000" \
|
|
||||||
-p 2222:22 \
|
|
||||||
-v "${FORGEJO_DATA_DIR}:/data" \
|
|
||||||
-e "FORGEJO__database__DB_TYPE=sqlite3" \
|
|
||||||
-e "FORGEJO__server__ROOT_URL=${forge_url}/" \
|
|
||||||
-e "FORGEJO__server__HTTP_PORT=3000" \
|
|
||||||
-e "FORGEJO__service__DISABLE_REGISTRATION=true" \
|
|
||||||
codeberg.org/forgejo/forgejo:11.0
|
|
||||||
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
|
|
||||||
echo -n "Waiting for Forgejo to start"
|
|
||||||
local retries=0
|
|
||||||
while ! curl -sf --max-time 3 "${forge_url}/api/v1/version" >/dev/null 2>&1; do
|
|
||||||
retries=$((retries + 1))
|
|
||||||
if [ "$retries" -gt 60 ]; then
|
|
||||||
echo ""
|
|
||||||
echo "Error: Forgejo did not become ready within 60s" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo -n "."
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo " ready"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Wait for Forgejo database to accept writes (API may be ready before DB is)
|
|
||||||
echo -n "Waiting for Forgejo database"
|
|
||||||
local db_ready=false
|
|
||||||
for _i in $(seq 1 30); do
|
|
||||||
if _forgejo_exec forgejo admin user list >/dev/null 2>&1; then
|
|
||||||
db_ready=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo -n "."
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
if [ "$db_ready" != true ]; then
|
|
||||||
echo "Error: Forgejo database not ready after 30s" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create admin user if it doesn't exist
|
|
||||||
local admin_user="disinto-admin"
|
|
||||||
local admin_pass
|
|
||||||
local env_file="${FACTORY_ROOT}/.env"
|
|
||||||
|
|
||||||
# Re-read persisted admin password if available (#158)
|
|
||||||
if grep -q '^FORGE_ADMIN_PASS=' "$env_file" 2>/dev/null; then
|
|
||||||
admin_pass=$(grep '^FORGE_ADMIN_PASS=' "$env_file" | head -1 | cut -d= -f2-)
|
|
||||||
fi
|
|
||||||
# Generate a fresh password only when none was persisted
|
|
||||||
if [ -z "${admin_pass:-}" ]; then
|
|
||||||
admin_pass="admin-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${admin_user}" >/dev/null 2>&1; then
|
|
||||||
echo "Creating admin user: ${admin_user}"
|
|
||||||
local create_output
|
|
||||||
if ! create_output=$(_forgejo_exec forgejo admin user create \
|
|
||||||
--admin \
|
|
||||||
--username "${admin_user}" \
|
|
||||||
--password "${admin_pass}" \
|
|
||||||
--email "admin@disinto.local" \
|
|
||||||
--must-change-password=false 2>&1); then
|
|
||||||
echo "Error: failed to create admin user '${admin_user}':" >&2
|
|
||||||
echo " ${create_output}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# Forgejo 11.x ignores --must-change-password=false on create;
|
|
||||||
# explicitly clear the flag so basic-auth token creation works.
|
|
||||||
_forgejo_exec forgejo admin user change-password \
|
|
||||||
--username "${admin_user}" \
|
|
||||||
--password "${admin_pass}" \
|
|
||||||
--must-change-password=false
|
|
||||||
|
|
||||||
# Verify admin user was actually created
|
|
||||||
if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${admin_user}" >/dev/null 2>&1; then
|
|
||||||
echo "Error: admin user '${admin_user}' not found after creation" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Persist admin password to .env for idempotent re-runs (#158)
|
|
||||||
if grep -q '^FORGE_ADMIN_PASS=' "$env_file" 2>/dev/null; then
|
|
||||||
sed -i "s|^FORGE_ADMIN_PASS=.*|FORGE_ADMIN_PASS=${admin_pass}|" "$env_file"
|
|
||||||
else
|
|
||||||
printf 'FORGE_ADMIN_PASS=%s\n' "$admin_pass" >> "$env_file"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Admin user: ${admin_user} (already exists)"
|
|
||||||
# Only reset password if basic auth fails (#158, #267)
|
|
||||||
# Forgejo 11.x may ignore --must-change-password=false, blocking token creation
|
|
||||||
if ! curl -sf --max-time 5 -u "${admin_user}:${admin_pass}" \
|
|
||||||
"${forge_url}/api/v1/user" >/dev/null 2>&1; then
|
|
||||||
_forgejo_exec forgejo admin user change-password \
|
|
||||||
--username "${admin_user}" \
|
|
||||||
--password "${admin_pass}" \
|
|
||||||
--must-change-password=false
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# Preserve password for Woodpecker OAuth2 token generation (#779)
|
|
||||||
_FORGE_ADMIN_PASS="$admin_pass"
|
|
||||||
|
|
||||||
# Create human user (disinto-admin) as site admin if it doesn't exist
|
|
||||||
local human_user="disinto-admin"
|
|
||||||
local human_pass
|
|
||||||
human_pass="admin-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)"
|
|
||||||
|
|
||||||
if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${human_user}" >/dev/null 2>&1; then
|
|
||||||
echo "Creating human user: ${human_user}"
|
|
||||||
local create_output
|
|
||||||
if ! create_output=$(_forgejo_exec forgejo admin user create \
|
|
||||||
--admin \
|
|
||||||
--username "${human_user}" \
|
|
||||||
--password "${human_pass}" \
|
|
||||||
--email "admin@disinto.local" \
|
|
||||||
--must-change-password=false 2>&1); then
|
|
||||||
echo "Error: failed to create human user '${human_user}':" >&2
|
|
||||||
echo " ${create_output}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# Forgejo 11.x ignores --must-change-password=false on create;
|
|
||||||
# explicitly clear the flag so basic-auth token creation works.
|
|
||||||
_forgejo_exec forgejo admin user change-password \
|
|
||||||
--username "${human_user}" \
|
|
||||||
--password "${human_pass}" \
|
|
||||||
--must-change-password=false
|
|
||||||
|
|
||||||
# Verify human user was actually created
|
|
||||||
if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${human_user}" >/dev/null 2>&1; then
|
|
||||||
echo "Error: human user '${human_user}' not found after creation" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " Human user '${human_user}' created as site admin"
|
|
||||||
else
|
|
||||||
echo "Human user: ${human_user} (already exists)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Delete existing admin token if present (token sha1 is only returned at creation time)
|
|
||||||
local existing_token_id
|
|
||||||
existing_token_id=$(curl -sf \
|
|
||||||
-u "${admin_user}:${admin_pass}" \
|
|
||||||
"${forge_url}/api/v1/users/${admin_user}/tokens" 2>/dev/null \
|
|
||||||
| jq -r '.[] | select(.name == "disinto-admin-token") | .id') || existing_token_id=""
|
|
||||||
if [ -n "$existing_token_id" ]; then
|
|
||||||
curl -sf -X DELETE \
|
|
||||||
-u "${admin_user}:${admin_pass}" \
|
|
||||||
"${forge_url}/api/v1/users/${admin_user}/tokens/${existing_token_id}" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create admin token (fresh, so sha1 is returned)
|
|
||||||
local admin_token
|
|
||||||
admin_token=$(curl -sf -X POST \
|
|
||||||
-u "${admin_user}:${admin_pass}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${forge_url}/api/v1/users/${admin_user}/tokens" \
|
|
||||||
-d '{"name":"disinto-admin-token","scopes":["all"]}' 2>/dev/null \
|
|
||||||
| jq -r '.sha1 // empty') || admin_token=""
|
|
||||||
|
|
||||||
if [ -z "$admin_token" ]; then
|
|
||||||
echo "Error: failed to obtain admin API token" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get or create human user token
|
|
||||||
local human_token
|
|
||||||
if curl -sf --max-time 5 "${forge_url}/api/v1/users/${human_user}" >/dev/null 2>&1; then
|
|
||||||
# Delete existing human token if present (token sha1 is only returned at creation time)
|
|
||||||
local existing_human_token_id
|
|
||||||
existing_human_token_id=$(curl -sf \
|
|
||||||
-u "${human_user}:${human_pass}" \
|
|
||||||
"${forge_url}/api/v1/users/${human_user}/tokens" 2>/dev/null \
|
|
||||||
| jq -r '.[] | select(.name == "disinto-human-token") | .id') || existing_human_token_id=""
|
|
||||||
if [ -n "$existing_human_token_id" ]; then
|
|
||||||
curl -sf -X DELETE \
|
|
||||||
-u "${human_user}:${human_pass}" \
|
|
||||||
"${forge_url}/api/v1/users/${human_user}/tokens/${existing_human_token_id}" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create human token (fresh, so sha1 is returned)
|
|
||||||
human_token=$(curl -sf -X POST \
|
|
||||||
-u "${human_user}:${human_pass}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${forge_url}/api/v1/users/${human_user}/tokens" \
|
|
||||||
-d '{"name":"disinto-human-token","scopes":["all"]}' 2>/dev/null \
|
|
||||||
| jq -r '.sha1 // empty') || human_token=""
|
|
||||||
|
|
||||||
if [ -n "$human_token" ]; then
|
|
||||||
# Store human token in .env
|
|
||||||
if grep -q '^HUMAN_TOKEN=' "$env_file" 2>/dev/null; then
|
|
||||||
sed -i "s|^HUMAN_TOKEN=.*|HUMAN_TOKEN=${human_token}|" "$env_file"
|
|
||||||
else
|
|
||||||
printf 'HUMAN_TOKEN=%s\n' "$human_token" >> "$env_file"
|
|
||||||
fi
|
|
||||||
export HUMAN_TOKEN="$human_token"
|
|
||||||
echo " Human token saved (HUMAN_TOKEN)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create bot users and tokens
|
|
||||||
# Each agent gets its own Forgejo account for identity and audit trail (#747).
|
|
||||||
# Map: bot-username -> env-var-name for the token
|
|
||||||
local -A bot_token_vars=(
|
|
||||||
[dev-bot]="FORGE_TOKEN"
|
|
||||||
[review-bot]="FORGE_REVIEW_TOKEN"
|
|
||||||
[planner-bot]="FORGE_PLANNER_TOKEN"
|
|
||||||
[gardener-bot]="FORGE_GARDENER_TOKEN"
|
|
||||||
[vault-bot]="FORGE_VAULT_TOKEN"
|
|
||||||
[supervisor-bot]="FORGE_SUPERVISOR_TOKEN"
|
|
||||||
[predictor-bot]="FORGE_PREDICTOR_TOKEN"
|
|
||||||
[architect-bot]="FORGE_ARCHITECT_TOKEN"
|
|
||||||
)
|
|
||||||
|
|
||||||
local bot_user bot_pass token token_var
|
|
||||||
|
|
||||||
for bot_user in dev-bot review-bot planner-bot gardener-bot vault-bot supervisor-bot predictor-bot architect-bot; do
|
|
||||||
bot_pass="bot-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)"
|
|
||||||
token_var="${bot_token_vars[$bot_user]}"
|
|
||||||
|
|
||||||
# Check if bot user exists
|
|
||||||
local user_exists=false
|
|
||||||
if curl -sf --max-time 5 \
|
|
||||||
-H "Authorization: token ${admin_token}" \
|
|
||||||
"${forge_url}/api/v1/users/${bot_user}" >/dev/null 2>&1; then
|
|
||||||
user_exists=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$user_exists" = false ]; then
|
|
||||||
echo "Creating bot user: ${bot_user}"
|
|
||||||
local create_output
|
|
||||||
if ! create_output=$(_forgejo_exec forgejo admin user create \
|
|
||||||
--username "${bot_user}" \
|
|
||||||
--password "${bot_pass}" \
|
|
||||||
--email "${bot_user}@disinto.local" \
|
|
||||||
--must-change-password=false 2>&1); then
|
|
||||||
echo "Error: failed to create bot user '${bot_user}':" >&2
|
|
||||||
echo " ${create_output}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# Forgejo 11.x ignores --must-change-password=false on create;
|
|
||||||
# explicitly clear the flag so basic-auth token creation works.
|
|
||||||
_forgejo_exec forgejo admin user change-password \
|
|
||||||
--username "${bot_user}" \
|
|
||||||
--password "${bot_pass}" \
|
|
||||||
--must-change-password=false
|
|
||||||
|
|
||||||
# Verify bot user was actually created
|
|
||||||
if ! curl -sf --max-time 5 \
|
|
||||||
-H "Authorization: token ${admin_token}" \
|
|
||||||
"${forge_url}/api/v1/users/${bot_user}" >/dev/null 2>&1; then
|
|
||||||
echo "Error: bot user '${bot_user}' not found after creation" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " ${bot_user} user created"
|
|
||||||
else
|
|
||||||
echo " ${bot_user} user exists (resetting password for token generation)"
|
|
||||||
# User exists but may not have a known password.
|
|
||||||
# Use admin API to reset the password so we can generate a new token.
|
|
||||||
_forgejo_exec forgejo admin user change-password \
|
|
||||||
--username "${bot_user}" \
|
|
||||||
--password "${bot_pass}" \
|
|
||||||
--must-change-password=false || {
|
|
||||||
echo "Error: failed to reset password for existing bot user '${bot_user}'" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Generate token via API (basic auth as the bot user — Forgejo requires
|
|
||||||
# basic auth on POST /users/{username}/tokens, token auth is rejected)
|
|
||||||
# First, try to delete existing tokens to avoid name collision
|
|
||||||
# Use bot user's own Basic Auth (we just set the password above)
|
|
||||||
local existing_token_ids
|
|
||||||
existing_token_ids=$(curl -sf \
|
|
||||||
-u "${bot_user}:${bot_pass}" \
|
|
||||||
"${forge_url}/api/v1/users/${bot_user}/tokens" 2>/dev/null \
|
|
||||||
| jq -r '.[].id // empty' 2>/dev/null) || existing_token_ids=""
|
|
||||||
|
|
||||||
# Delete any existing tokens for this user
|
|
||||||
if [ -n "$existing_token_ids" ]; then
|
|
||||||
while IFS= read -r tid; do
|
|
||||||
[ -n "$tid" ] && curl -sf -X DELETE \
|
|
||||||
-u "${bot_user}:${bot_pass}" \
|
|
||||||
"${forge_url}/api/v1/users/${bot_user}/tokens/${tid}" >/dev/null 2>&1 || true
|
|
||||||
done <<< "$existing_token_ids"
|
|
||||||
fi
|
|
||||||
|
|
||||||
token=$(curl -sf -X POST \
|
|
||||||
-u "${bot_user}:${bot_pass}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${forge_url}/api/v1/users/${bot_user}/tokens" \
|
|
||||||
-d "{\"name\":\"disinto-${bot_user}-token\",\"scopes\":[\"all\"]}" 2>/dev/null \
|
|
||||||
| jq -r '.sha1 // empty') || token=""
|
|
||||||
|
|
||||||
if [ -z "$token" ]; then
|
|
||||||
echo "Error: failed to create API token for '${bot_user}'" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Store token in .env under the per-agent variable name
|
|
||||||
if grep -q "^${token_var}=" "$env_file" 2>/dev/null; then
|
|
||||||
sed -i "s|^${token_var}=.*|${token_var}=${token}|" "$env_file"
|
|
||||||
else
|
|
||||||
printf '%s=%s\n' "$token_var" "$token" >> "$env_file"
|
|
||||||
fi
|
|
||||||
export "${token_var}=${token}"
|
|
||||||
echo " ${bot_user} token generated and saved (${token_var})"
|
|
||||||
|
|
||||||
# Backwards-compat aliases for dev-bot and review-bot
|
|
||||||
if [ "$bot_user" = "dev-bot" ]; then
|
|
||||||
export CODEBERG_TOKEN="$token"
|
|
||||||
elif [ "$bot_user" = "review-bot" ]; then
|
|
||||||
export REVIEW_BOT_TOKEN="$token"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Store FORGE_URL in .env if not already present
|
|
||||||
if ! grep -q '^FORGE_URL=' "$env_file" 2>/dev/null; then
|
|
||||||
printf 'FORGE_URL=%s\n' "$forge_url" >> "$env_file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create the repo on Forgejo if it doesn't exist
|
|
||||||
local org_name="${repo_slug%%/*}"
|
|
||||||
local repo_name="${repo_slug##*/}"
|
|
||||||
|
|
||||||
# Check if repo already exists
|
|
||||||
if ! curl -sf --max-time 5 \
|
|
||||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
|
||||||
"${forge_url}/api/v1/repos/${repo_slug}" >/dev/null 2>&1; then
|
|
||||||
|
|
||||||
# Try creating org first (ignore if exists)
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${forge_url}/api/v1/orgs" \
|
|
||||||
-d "{\"username\":\"${org_name}\",\"visibility\":\"public\"}" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Create repo under org
|
|
||||||
if ! curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${forge_url}/api/v1/orgs/${org_name}/repos" \
|
|
||||||
-d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1; then
|
|
||||||
# Fallback: create under the human user namespace using admin endpoint
|
|
||||||
if [ -n "${admin_token:-}" ]; then
|
|
||||||
if ! curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${admin_token}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${forge_url}/api/v1/admin/users/${org_name}/repos" \
|
|
||||||
-d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1; then
|
|
||||||
echo "Error: failed to create repo '${repo_slug}' on Forgejo (admin endpoint)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
elif [ -n "${HUMAN_TOKEN:-}" ]; then
|
|
||||||
if ! curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${HUMAN_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${forge_url}/api/v1/user/repos" \
|
|
||||||
-d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1; then
|
|
||||||
echo "Error: failed to create repo '${repo_slug}' on Forgejo (user endpoint)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Error: failed to create repo '${repo_slug}' — no admin or human token available" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add all bot users as collaborators with appropriate permissions
|
|
||||||
# dev-bot: write (PR creation via lib/vault.sh)
|
|
||||||
# review-bot: read (PR review)
|
|
||||||
# planner-bot: write (prerequisites.md, memory)
|
|
||||||
# gardener-bot: write (backlog grooming)
|
|
||||||
# vault-bot: write (vault items)
|
|
||||||
# supervisor-bot: read (health monitoring)
|
|
||||||
# predictor-bot: read (pattern detection)
|
|
||||||
# architect-bot: write (sprint PRs)
|
|
||||||
local bot_user bot_perm
|
|
||||||
declare -A bot_permissions=(
|
|
||||||
[dev-bot]="write"
|
|
||||||
[review-bot]="read"
|
|
||||||
[planner-bot]="write"
|
|
||||||
[gardener-bot]="write"
|
|
||||||
[vault-bot]="write"
|
|
||||||
[supervisor-bot]="read"
|
|
||||||
[predictor-bot]="read"
|
|
||||||
[architect-bot]="write"
|
|
||||||
)
|
|
||||||
for bot_user in "${!bot_permissions[@]}"; do
|
|
||||||
bot_perm="${bot_permissions[$bot_user]}"
|
|
||||||
curl -sf -X PUT \
|
|
||||||
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${forge_url}/api/v1/repos/${repo_slug}/collaborators/${bot_user}" \
|
|
||||||
-d "{\"permission\":\"${bot_perm}\"}" >/dev/null 2>&1 || true
|
|
||||||
done
|
|
||||||
|
|
||||||
# Add disinto-admin as admin collaborator
|
|
||||||
curl -sf -X PUT \
|
|
||||||
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${forge_url}/api/v1/repos/${repo_slug}/collaborators/disinto-admin" \
|
|
||||||
-d '{"permission":"admin"}' >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
echo "Repo: ${repo_slug} created on Forgejo"
|
|
||||||
else
|
|
||||||
echo "Repo: ${repo_slug} (already exists on Forgejo)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Forge: ${forge_url} (ready)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create and seed the {project}-ops repo on Forgejo with initial directory structure.
|
# Create and seed the {project}-ops repo on Forgejo with initial directory structure.
|
||||||
# The ops repo holds operational data: vault items, journals, evidence, prerequisites.
|
# The ops repo holds operational data: vault items, journals, evidence, prerequisites.
|
||||||
# ops repo setup is now in lib/ops-setup.sh
|
# ops repo setup is now in lib/ops-setup.sh
|
||||||
|
|
|
||||||
494
lib/forge-setup.sh
Normal file
494
lib/forge-setup.sh
Normal file
|
|
@ -0,0 +1,494 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# forge-setup.sh — setup_forge() and helpers for Forgejo provisioning
|
||||||
|
#
|
||||||
|
# Handles admin user creation, bot user creation, token generation,
|
||||||
|
# password resets, repo creation, and collaborator setup.
|
||||||
|
#
|
||||||
|
# Globals expected (asserted by _load_init_context):
|
||||||
|
# FORGE_URL - Forge instance URL (e.g. http://localhost:3000)
|
||||||
|
# FACTORY_ROOT - Root of the disinto factory
|
||||||
|
# PRIMARY_BRANCH - Primary branch name (e.g. main)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# source "${FACTORY_ROOT}/lib/forge-setup.sh"
|
||||||
|
# setup_forge <forge_url> <repo_slug>
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Assert required globals are set before using this module.
|
||||||
|
_load_init_context() {
|
||||||
|
local missing=()
|
||||||
|
[ -z "${FORGE_URL:-}" ] && missing+=("FORGE_URL")
|
||||||
|
[ -z "${FACTORY_ROOT:-}" ] && missing+=("FACTORY_ROOT")
|
||||||
|
[ -z "${PRIMARY_BRANCH:-}" ] && missing+=("PRIMARY_BRANCH")
|
||||||
|
if [ "${#missing[@]}" -gt 0 ]; then
|
||||||
|
echo "Error: forge-setup.sh requires these globals to be set: ${missing[*]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Execute a command in the Forgejo container (for admin operations)
|
||||||
|
_forgejo_exec() {
|
||||||
|
local use_bare="${DISINTO_BARE:-false}"
|
||||||
|
if [ "$use_bare" = true ]; then
|
||||||
|
docker exec -u git disinto-forgejo "$@"
|
||||||
|
else
|
||||||
|
docker compose -f "${FACTORY_ROOT}/docker-compose.yml" exec -T -u git forgejo "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Provision or connect to a local Forgejo instance.
|
||||||
|
# Creates admin + bot users, generates API tokens, stores in .env.
|
||||||
|
# When $DISINTO_BARE is set, uses standalone docker run; otherwise uses compose.
|
||||||
|
setup_forge() {
|
||||||
|
local forge_url="$1"
|
||||||
|
local repo_slug="$2"
|
||||||
|
local use_bare="${DISINTO_BARE:-false}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "── Forge setup ────────────────────────────────────────"
|
||||||
|
|
||||||
|
# Check if Forgejo is already running
|
||||||
|
if curl -sf --max-time 5 "${forge_url}/api/v1/version" >/dev/null 2>&1; then
|
||||||
|
echo "Forgejo: ${forge_url} (already running)"
|
||||||
|
else
|
||||||
|
echo "Forgejo not reachable at ${forge_url}"
|
||||||
|
echo "Starting Forgejo via Docker..."
|
||||||
|
|
||||||
|
if ! command -v docker &>/dev/null; then
|
||||||
|
echo "Error: docker not found — needed to provision Forgejo" >&2
|
||||||
|
echo " Install Docker or start Forgejo manually at ${forge_url}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract port from forge_url
|
||||||
|
local forge_port
|
||||||
|
forge_port=$(printf '%s' "$forge_url" | sed -E 's|.*:([0-9]+)/?$|\1|')
|
||||||
|
forge_port="${forge_port:-3000}"
|
||||||
|
|
||||||
|
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
|
||||||
|
docker start disinto-forgejo >/dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
docker run -d \
|
||||||
|
--name disinto-forgejo \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p "${forge_port}:3000" \
|
||||||
|
-p 2222:22 \
|
||||||
|
-v "${FORGEJO_DATA_DIR}:/data" \
|
||||||
|
-e "FORGEJO__database__DB_TYPE=sqlite3" \
|
||||||
|
-e "FORGEJO__server__ROOT_URL=${forge_url}/" \
|
||||||
|
-e "FORGEJO__server__HTTP_PORT=3000" \
|
||||||
|
-e "FORGEJO__service__DISABLE_REGISTRATION=true" \
|
||||||
|
codeberg.org/forgejo/forgejo:11.0
|
||||||
|
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
|
||||||
|
echo -n "Waiting for Forgejo to start"
|
||||||
|
local retries=0
|
||||||
|
while ! curl -sf --max-time 3 "${forge_url}/api/v1/version" >/dev/null 2>&1; do
|
||||||
|
retries=$((retries + 1))
|
||||||
|
if [ "$retries" -gt 60 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Error: Forgejo did not become ready within 60s" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -n "."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo " ready"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait for Forgejo database to accept writes (API may be ready before DB is)
|
||||||
|
echo -n "Waiting for Forgejo database"
|
||||||
|
local db_ready=false
|
||||||
|
for _i in $(seq 1 30); do
|
||||||
|
if _forgejo_exec forgejo admin user list >/dev/null 2>&1; then
|
||||||
|
db_ready=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo -n "."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
if [ "$db_ready" != true ]; then
|
||||||
|
echo "Error: Forgejo database not ready after 30s" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create admin user if it doesn't exist
|
||||||
|
local admin_user="disinto-admin"
|
||||||
|
local admin_pass
|
||||||
|
local env_file="${FACTORY_ROOT}/.env"
|
||||||
|
|
||||||
|
# Re-read persisted admin password if available (#158)
|
||||||
|
if grep -q '^FORGE_ADMIN_PASS=' "$env_file" 2>/dev/null; then
|
||||||
|
admin_pass=$(grep '^FORGE_ADMIN_PASS=' "$env_file" | head -1 | cut -d= -f2-)
|
||||||
|
fi
|
||||||
|
# Generate a fresh password only when none was persisted
|
||||||
|
if [ -z "${admin_pass:-}" ]; then
|
||||||
|
admin_pass="admin-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${admin_user}" >/dev/null 2>&1; then
|
||||||
|
echo "Creating admin user: ${admin_user}"
|
||||||
|
local create_output
|
||||||
|
if ! create_output=$(_forgejo_exec forgejo admin user create \
|
||||||
|
--admin \
|
||||||
|
--username "${admin_user}" \
|
||||||
|
--password "${admin_pass}" \
|
||||||
|
--email "admin@disinto.local" \
|
||||||
|
--must-change-password=false 2>&1); then
|
||||||
|
echo "Error: failed to create admin user '${admin_user}':" >&2
|
||||||
|
echo " ${create_output}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Forgejo 11.x ignores --must-change-password=false on create;
|
||||||
|
# explicitly clear the flag so basic-auth token creation works.
|
||||||
|
_forgejo_exec forgejo admin user change-password \
|
||||||
|
--username "${admin_user}" \
|
||||||
|
--password "${admin_pass}" \
|
||||||
|
--must-change-password=false
|
||||||
|
|
||||||
|
# Verify admin user was actually created
|
||||||
|
if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${admin_user}" >/dev/null 2>&1; then
|
||||||
|
echo "Error: admin user '${admin_user}' not found after creation" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Persist admin password to .env for idempotent re-runs (#158)
|
||||||
|
if grep -q '^FORGE_ADMIN_PASS=' "$env_file" 2>/dev/null; then
|
||||||
|
sed -i "s|^FORGE_ADMIN_PASS=.*|FORGE_ADMIN_PASS=${admin_pass}|" "$env_file"
|
||||||
|
else
|
||||||
|
printf 'FORGE_ADMIN_PASS=%s\n' "$admin_pass" >> "$env_file"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Admin user: ${admin_user} (already exists)"
|
||||||
|
# Only reset password if basic auth fails (#158, #267)
|
||||||
|
# Forgejo 11.x may ignore --must-change-password=false, blocking token creation
|
||||||
|
if ! curl -sf --max-time 5 -u "${admin_user}:${admin_pass}" \
|
||||||
|
"${forge_url}/api/v1/user" >/dev/null 2>&1; then
|
||||||
|
_forgejo_exec forgejo admin user change-password \
|
||||||
|
--username "${admin_user}" \
|
||||||
|
--password "${admin_pass}" \
|
||||||
|
--must-change-password=false
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Preserve password for Woodpecker OAuth2 token generation (#779)
|
||||||
|
_FORGE_ADMIN_PASS="$admin_pass"
|
||||||
|
|
||||||
|
# Create human user (disinto-admin) as site admin if it doesn't exist
|
||||||
|
local human_user="disinto-admin"
|
||||||
|
local human_pass
|
||||||
|
human_pass="admin-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)"
|
||||||
|
|
||||||
|
if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${human_user}" >/dev/null 2>&1; then
|
||||||
|
echo "Creating human user: ${human_user}"
|
||||||
|
local create_output
|
||||||
|
if ! create_output=$(_forgejo_exec forgejo admin user create \
|
||||||
|
--admin \
|
||||||
|
--username "${human_user}" \
|
||||||
|
--password "${human_pass}" \
|
||||||
|
--email "admin@disinto.local" \
|
||||||
|
--must-change-password=false 2>&1); then
|
||||||
|
echo "Error: failed to create human user '${human_user}':" >&2
|
||||||
|
echo " ${create_output}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Forgejo 11.x ignores --must-change-password=false on create;
|
||||||
|
# explicitly clear the flag so basic-auth token creation works.
|
||||||
|
_forgejo_exec forgejo admin user change-password \
|
||||||
|
--username "${human_user}" \
|
||||||
|
--password "${human_pass}" \
|
||||||
|
--must-change-password=false
|
||||||
|
|
||||||
|
# Verify human user was actually created
|
||||||
|
if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${human_user}" >/dev/null 2>&1; then
|
||||||
|
echo "Error: human user '${human_user}' not found after creation" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " Human user '${human_user}' created as site admin"
|
||||||
|
else
|
||||||
|
echo "Human user: ${human_user} (already exists)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete existing admin token if present (token sha1 is only returned at creation time)
|
||||||
|
local existing_token_id
|
||||||
|
existing_token_id=$(curl -sf \
|
||||||
|
-u "${admin_user}:${admin_pass}" \
|
||||||
|
"${forge_url}/api/v1/users/${admin_user}/tokens" 2>/dev/null \
|
||||||
|
| jq -r '.[] | select(.name == "disinto-admin-token") | .id') || existing_token_id=""
|
||||||
|
if [ -n "$existing_token_id" ]; then
|
||||||
|
curl -sf -X DELETE \
|
||||||
|
-u "${admin_user}:${admin_pass}" \
|
||||||
|
"${forge_url}/api/v1/users/${admin_user}/tokens/${existing_token_id}" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create admin token (fresh, so sha1 is returned)
|
||||||
|
local admin_token
|
||||||
|
admin_token=$(curl -sf -X POST \
|
||||||
|
-u "${admin_user}:${admin_pass}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${forge_url}/api/v1/users/${admin_user}/tokens" \
|
||||||
|
-d '{"name":"disinto-admin-token","scopes":["all"]}' 2>/dev/null \
|
||||||
|
| jq -r '.sha1 // empty') || admin_token=""
|
||||||
|
|
||||||
|
if [ -z "$admin_token" ]; then
|
||||||
|
echo "Error: failed to obtain admin API token" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get or create human user token
|
||||||
|
local human_token
|
||||||
|
if curl -sf --max-time 5 "${forge_url}/api/v1/users/${human_user}" >/dev/null 2>&1; then
|
||||||
|
# Delete existing human token if present (token sha1 is only returned at creation time)
|
||||||
|
local existing_human_token_id
|
||||||
|
existing_human_token_id=$(curl -sf \
|
||||||
|
-u "${human_user}:${human_pass}" \
|
||||||
|
"${forge_url}/api/v1/users/${human_user}/tokens" 2>/dev/null \
|
||||||
|
| jq -r '.[] | select(.name == "disinto-human-token") | .id') || existing_human_token_id=""
|
||||||
|
if [ -n "$existing_human_token_id" ]; then
|
||||||
|
curl -sf -X DELETE \
|
||||||
|
-u "${human_user}:${human_pass}" \
|
||||||
|
"${forge_url}/api/v1/users/${human_user}/tokens/${existing_human_token_id}" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create human token (fresh, so sha1 is returned)
|
||||||
|
human_token=$(curl -sf -X POST \
|
||||||
|
-u "${human_user}:${human_pass}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${forge_url}/api/v1/users/${human_user}/tokens" \
|
||||||
|
-d '{"name":"disinto-human-token","scopes":["all"]}' 2>/dev/null \
|
||||||
|
| jq -r '.sha1 // empty') || human_token=""
|
||||||
|
|
||||||
|
if [ -n "$human_token" ]; then
|
||||||
|
# Store human token in .env
|
||||||
|
if grep -q '^HUMAN_TOKEN=' "$env_file" 2>/dev/null; then
|
||||||
|
sed -i "s|^HUMAN_TOKEN=.*|HUMAN_TOKEN=${human_token}|" "$env_file"
|
||||||
|
else
|
||||||
|
printf 'HUMAN_TOKEN=%s\n' "$human_token" >> "$env_file"
|
||||||
|
fi
|
||||||
|
export HUMAN_TOKEN="$human_token"
|
||||||
|
echo " Human token saved (HUMAN_TOKEN)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create bot users and tokens
|
||||||
|
# Each agent gets its own Forgejo account for identity and audit trail (#747).
|
||||||
|
# Map: bot-username -> env-var-name for the token
|
||||||
|
local -A bot_token_vars=(
|
||||||
|
[dev-bot]="FORGE_TOKEN"
|
||||||
|
[review-bot]="FORGE_REVIEW_TOKEN"
|
||||||
|
[planner-bot]="FORGE_PLANNER_TOKEN"
|
||||||
|
[gardener-bot]="FORGE_GARDENER_TOKEN"
|
||||||
|
[vault-bot]="FORGE_VAULT_TOKEN"
|
||||||
|
[supervisor-bot]="FORGE_SUPERVISOR_TOKEN"
|
||||||
|
[predictor-bot]="FORGE_PREDICTOR_TOKEN"
|
||||||
|
[architect-bot]="FORGE_ARCHITECT_TOKEN"
|
||||||
|
)
|
||||||
|
|
||||||
|
local bot_user bot_pass token token_var
|
||||||
|
|
||||||
|
for bot_user in dev-bot review-bot planner-bot gardener-bot vault-bot supervisor-bot predictor-bot architect-bot; do
|
||||||
|
bot_pass="bot-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)"
|
||||||
|
token_var="${bot_token_vars[$bot_user]}"
|
||||||
|
|
||||||
|
# Check if bot user exists
|
||||||
|
local user_exists=false
|
||||||
|
if curl -sf --max-time 5 \
|
||||||
|
-H "Authorization: token ${admin_token}" \
|
||||||
|
"${forge_url}/api/v1/users/${bot_user}" >/dev/null 2>&1; then
|
||||||
|
user_exists=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$user_exists" = false ]; then
|
||||||
|
echo "Creating bot user: ${bot_user}"
|
||||||
|
local create_output
|
||||||
|
if ! create_output=$(_forgejo_exec forgejo admin user create \
|
||||||
|
--username "${bot_user}" \
|
||||||
|
--password "${bot_pass}" \
|
||||||
|
--email "${bot_user}@disinto.local" \
|
||||||
|
--must-change-password=false 2>&1); then
|
||||||
|
echo "Error: failed to create bot user '${bot_user}':" >&2
|
||||||
|
echo " ${create_output}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Forgejo 11.x ignores --must-change-password=false on create;
|
||||||
|
# explicitly clear the flag so basic-auth token creation works.
|
||||||
|
_forgejo_exec forgejo admin user change-password \
|
||||||
|
--username "${bot_user}" \
|
||||||
|
--password "${bot_pass}" \
|
||||||
|
--must-change-password=false
|
||||||
|
|
||||||
|
# Verify bot user was actually created
|
||||||
|
if ! curl -sf --max-time 5 \
|
||||||
|
-H "Authorization: token ${admin_token}" \
|
||||||
|
"${forge_url}/api/v1/users/${bot_user}" >/dev/null 2>&1; then
|
||||||
|
echo "Error: bot user '${bot_user}' not found after creation" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " ${bot_user} user created"
|
||||||
|
else
|
||||||
|
echo " ${bot_user} user exists (resetting password for token generation)"
|
||||||
|
# User exists but may not have a known password.
|
||||||
|
# Use admin API to reset the password so we can generate a new token.
|
||||||
|
_forgejo_exec forgejo admin user change-password \
|
||||||
|
--username "${bot_user}" \
|
||||||
|
--password "${bot_pass}" \
|
||||||
|
--must-change-password=false || {
|
||||||
|
echo "Error: failed to reset password for existing bot user '${bot_user}'" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate token via API (basic auth as the bot user — Forgejo requires
|
||||||
|
# basic auth on POST /users/{username}/tokens, token auth is rejected)
|
||||||
|
# First, try to delete existing tokens to avoid name collision
|
||||||
|
# Use bot user's own Basic Auth (we just set the password above)
|
||||||
|
local existing_token_ids
|
||||||
|
existing_token_ids=$(curl -sf \
|
||||||
|
-u "${bot_user}:${bot_pass}" \
|
||||||
|
"${forge_url}/api/v1/users/${bot_user}/tokens" 2>/dev/null \
|
||||||
|
| jq -r '.[].id // empty' 2>/dev/null) || existing_token_ids=""
|
||||||
|
|
||||||
|
# Delete any existing tokens for this user
|
||||||
|
if [ -n "$existing_token_ids" ]; then
|
||||||
|
while IFS= read -r tid; do
|
||||||
|
[ -n "$tid" ] && curl -sf -X DELETE \
|
||||||
|
-u "${bot_user}:${bot_pass}" \
|
||||||
|
"${forge_url}/api/v1/users/${bot_user}/tokens/${tid}" >/dev/null 2>&1 || true
|
||||||
|
done <<< "$existing_token_ids"
|
||||||
|
fi
|
||||||
|
|
||||||
|
token=$(curl -sf -X POST \
|
||||||
|
-u "${bot_user}:${bot_pass}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${forge_url}/api/v1/users/${bot_user}/tokens" \
|
||||||
|
-d "{\"name\":\"disinto-${bot_user}-token\",\"scopes\":[\"all\"]}" 2>/dev/null \
|
||||||
|
| jq -r '.sha1 // empty') || token=""
|
||||||
|
|
||||||
|
if [ -z "$token" ]; then
|
||||||
|
echo "Error: failed to create API token for '${bot_user}'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Store token in .env under the per-agent variable name
|
||||||
|
if grep -q "^${token_var}=" "$env_file" 2>/dev/null; then
|
||||||
|
sed -i "s|^${token_var}=.*|${token_var}=${token}|" "$env_file"
|
||||||
|
else
|
||||||
|
printf '%s=%s\n' "$token_var" "$token" >> "$env_file"
|
||||||
|
fi
|
||||||
|
export "${token_var}=${token}"
|
||||||
|
echo " ${bot_user} token generated and saved (${token_var})"
|
||||||
|
|
||||||
|
# Backwards-compat aliases for dev-bot and review-bot
|
||||||
|
if [ "$bot_user" = "dev-bot" ]; then
|
||||||
|
export CODEBERG_TOKEN="$token"
|
||||||
|
elif [ "$bot_user" = "review-bot" ]; then
|
||||||
|
export REVIEW_BOT_TOKEN="$token"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Store FORGE_URL in .env if not already present
|
||||||
|
if ! grep -q '^FORGE_URL=' "$env_file" 2>/dev/null; then
|
||||||
|
printf 'FORGE_URL=%s\n' "$forge_url" >> "$env_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create the repo on Forgejo if it doesn't exist
|
||||||
|
local org_name="${repo_slug%%/*}"
|
||||||
|
local repo_name="${repo_slug##*/}"
|
||||||
|
|
||||||
|
# Check if repo already exists
|
||||||
|
if ! curl -sf --max-time 5 \
|
||||||
|
-H "Authorization: token ${FORGE_TOKEN}" \
|
||||||
|
"${forge_url}/api/v1/repos/${repo_slug}" >/dev/null 2>&1; then
|
||||||
|
|
||||||
|
# Try creating org first (ignore if exists)
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${forge_url}/api/v1/orgs" \
|
||||||
|
-d "{\"username\":\"${org_name}\",\"visibility\":\"public\"}" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Create repo under org
|
||||||
|
if ! curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${forge_url}/api/v1/orgs/${org_name}/repos" \
|
||||||
|
-d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1; then
|
||||||
|
# Fallback: create under the human user namespace using admin endpoint
|
||||||
|
if [ -n "${admin_token:-}" ]; then
|
||||||
|
if ! curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${admin_token}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${forge_url}/api/v1/admin/users/${org_name}/repos" \
|
||||||
|
-d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1; then
|
||||||
|
echo "Error: failed to create repo '${repo_slug}' on Forgejo (admin endpoint)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [ -n "${HUMAN_TOKEN:-}" ]; then
|
||||||
|
if ! curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${HUMAN_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${forge_url}/api/v1/user/repos" \
|
||||||
|
-d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1; then
|
||||||
|
echo "Error: failed to create repo '${repo_slug}' on Forgejo (user endpoint)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Error: failed to create repo '${repo_slug}' — no admin or human token available" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add all bot users as collaborators with appropriate permissions
|
||||||
|
# dev-bot: write (PR creation via lib/vault.sh)
|
||||||
|
# review-bot: read (PR review)
|
||||||
|
# planner-bot: write (prerequisites.md, memory)
|
||||||
|
# gardener-bot: write (backlog grooming)
|
||||||
|
# vault-bot: write (vault items)
|
||||||
|
# supervisor-bot: read (health monitoring)
|
||||||
|
# predictor-bot: read (pattern detection)
|
||||||
|
# architect-bot: write (sprint PRs)
|
||||||
|
local bot_perm
|
||||||
|
declare -A bot_permissions=(
|
||||||
|
[dev-bot]="write"
|
||||||
|
[review-bot]="read"
|
||||||
|
[planner-bot]="write"
|
||||||
|
[gardener-bot]="write"
|
||||||
|
[vault-bot]="write"
|
||||||
|
[supervisor-bot]="read"
|
||||||
|
[predictor-bot]="read"
|
||||||
|
[architect-bot]="write"
|
||||||
|
)
|
||||||
|
for bot_user in "${!bot_permissions[@]}"; do
|
||||||
|
bot_perm="${bot_permissions[$bot_user]}"
|
||||||
|
curl -sf -X PUT \
|
||||||
|
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${forge_url}/api/v1/repos/${repo_slug}/collaborators/${bot_user}" \
|
||||||
|
-d "{\"permission\":\"${bot_perm}\"}" >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
|
||||||
|
# Add disinto-admin as admin collaborator
|
||||||
|
curl -sf -X PUT \
|
||||||
|
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${forge_url}/api/v1/repos/${repo_slug}/collaborators/disinto-admin" \
|
||||||
|
-d '{"permission":"admin"}' >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
echo "Repo: ${repo_slug} created on Forgejo"
|
||||||
|
else
|
||||||
|
echo "Repo: ${repo_slug} (already exists on Forgejo)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Forge: ${forge_url} (ready)"
|
||||||
|
}
|
||||||
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