disinto/lib/forge-setup.sh
Claude 7070fb960a
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/smoke-init Pipeline was successful
fix: refactor: extract setup_forge() from bin/disinto into lib/forge-setup.sh (#298)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 18:02:26 +00:00

480 lines
18 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
# forge-setup.sh — Forgejo provisioning helpers
#
# Source from bin/disinto: source "${FACTORY_ROOT}/lib/forge-setup.sh"
# Requires globals: FACTORY_ROOT (set by env.sh or the caller)
# Call _load_init_context before using this library standalone to assert
# that FORGE_URL, FACTORY_ROOT, and PRIMARY_BRANCH are all set.
# Assert required globals are set (call before using this library standalone).
_load_init_context() {
: "${FORGE_URL:?FORGE_URL must be set}"
: "${FACTORY_ROOT:?FACTORY_ROOT must be set}"
: "${PRIMARY_BRANCH:?PRIMARY_BRANCH must be set}"
}
# 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_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)"
}