fix: refactor: extract setup_ops_repo() from bin/disinto into lib/ops-setup.sh (#299) #305

Merged
dev-bot merged 1 commit from fix/issue-299 into main 2026-04-06 18:09:03 +00:00
2 changed files with 227 additions and 202 deletions

View file

@ -25,6 +25,7 @@ set -euo pipefail
FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
source "${FACTORY_ROOT}/lib/env.sh"
source "${FACTORY_ROOT}/lib/ops-setup.sh"
# ── Helpers ──────────────────────────────────────────────────────────────────
@ -1031,208 +1032,7 @@ setup_forge() {
# Create and seed the {project}-ops repo on Forgejo with initial directory structure.
# The ops repo holds operational data: vault items, journals, evidence, prerequisites.
setup_ops_repo() {
local forge_url="$1" ops_slug="$2" ops_root="$3" primary_branch="${4:-main}"
local org_name="${ops_slug%%/*}"
local ops_name="${ops_slug##*/}"
echo ""
echo "── Ops repo setup ─────────────────────────────────────"
# Determine the actual ops repo location by searching across possible namespaces
# This handles cases where the repo was created under a different namespace
# due to past bugs (e.g., dev-bot/disinto-ops instead of disinto-admin/disinto-ops)
local actual_ops_slug=""
local -a possible_namespaces=( "$org_name" "dev-bot" "disinto-admin" )
local http_code
for ns in "${possible_namespaces[@]}"; do
slug="${ns}/${ops_name}"
if curl -sf --max-time 5 \
-H "Authorization: token ${FORGE_TOKEN}" \
"${forge_url}/api/v1/repos/${slug}" >/dev/null 2>&1; then
actual_ops_slug="$slug"
echo "Ops repo: ${slug} (found at ${slug})"
break
fi
done
# If not found, try to create it in the configured namespace
if [ -z "$actual_ops_slug" ]; then
echo "Creating ops repo in namespace: ${org_name}"
# Create org if it doesn't exist
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
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\":\"${ops_name}\",\"auto_init\":true,\"default_branch\":\"${primary_branch}\",\"description\":\"Operational data for ${org_name}/${ops_name%-ops}\"}" >/dev/null 2>&1; then
actual_ops_slug="${org_name}/${ops_name}"
echo "Ops repo: ${actual_ops_slug} created on Forgejo"
else
# Fallback: use admin API to create repo under the target namespace
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST \
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
-H "Content-Type: application/json" \
"${forge_url}/api/v1/admin/users/${org_name}/repos" \
-d "{\"name\":\"${ops_name}\",\"auto_init\":true,\"default_branch\":\"${primary_branch}\",\"description\":\"Operational data for ${org_name}/${ops_name%-ops}\"}" 2>/dev/null || echo "0")
if [ "$http_code" = "201" ]; then
actual_ops_slug="${org_name}/${ops_name}"
echo "Ops repo: ${actual_ops_slug} created on Forgejo (via admin API)"
else
echo "Error: failed to create ops repo '${org_name}/${ops_name}' (HTTP ${http_code})" >&2
return 1
fi
fi
fi
# Configure collaborators on the ops repo
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"
)
# Add all bot users as collaborators with appropriate permissions
# vault branch protection (#77) requires:
# - Admin-only merge to main (enforced by admin_enforced: true)
# - Bots can push branches and create PRs, but cannot merge
for bot_user in "${!bot_permissions[@]}"; do
bot_perm="${bot_permissions[$bot_user]}"
if curl -sf -X PUT \
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
-H "Content-Type: application/json" \
"${forge_url}/api/v1/repos/${actual_ops_slug}/collaborators/${bot_user}" \
-d "{\"permission\":\"${bot_perm}\"}" >/dev/null 2>&1; then
echo " + ${bot_user} = ${bot_perm} collaborator"
else
echo " ! ${bot_user} = ${bot_perm} (already set or failed)"
fi
done
# Add disinto-admin as admin collaborator
if curl -sf -X PUT \
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
-H "Content-Type: application/json" \
"${forge_url}/api/v1/repos/${actual_ops_slug}/collaborators/disinto-admin" \
-d '{"permission":"admin"}' >/dev/null 2>&1; then
echo " + disinto-admin = admin collaborator"
else
echo " ! disinto-admin = admin (already set or failed)"
fi
# Clone ops repo locally if not present
if [ ! -d "${ops_root}/.git" ]; then
local auth_url
auth_url=$(printf '%s' "$forge_url" | sed "s|://|://dev-bot:${FORGE_TOKEN}@|")
local clone_url="${auth_url}/${actual_ops_slug}.git"
echo "Cloning: ops repo -> ${ops_root}"
if git clone --quiet "$clone_url" "$ops_root" 2>/dev/null; then
echo "Ops repo: ${actual_ops_slug} cloned successfully"
else
echo "Initializing: ops repo at ${ops_root}"
mkdir -p "$ops_root"
git -C "$ops_root" init --initial-branch="${primary_branch}" -q
# Set remote to the actual ops repo location
git -C "$ops_root" remote add origin "${forge_url}/${actual_ops_slug}.git"
echo "Ops repo: ${actual_ops_slug} initialized locally"
fi
else
echo "Ops repo: ${ops_root} (already exists locally)"
# Verify remote is correct
local current_remote
current_remote=$(git -C "$ops_root" remote get-url origin 2>/dev/null || true)
local expected_remote="${forge_url}/${actual_ops_slug}.git"
if [ -n "$current_remote" ] && [ "$current_remote" != "$expected_remote" ]; then
echo " Fixing: remote URL from ${current_remote} to ${expected_remote}"
git -C "$ops_root" remote set-url origin "$expected_remote"
fi
fi
# Seed directory structure
local seeded=false
mkdir -p "${ops_root}/vault/pending"
mkdir -p "${ops_root}/vault/approved"
mkdir -p "${ops_root}/vault/fired"
mkdir -p "${ops_root}/vault/rejected"
mkdir -p "${ops_root}/knowledge"
mkdir -p "${ops_root}/evidence/engagement"
if [ ! -f "${ops_root}/README.md" ]; then
cat > "${ops_root}/README.md" <<OPSEOF
# ${ops_name}
Operational data for the ${ops_name%-ops} project.
## Structure
\`\`\`
${ops_name}/
├── vault/
│ ├── pending/ # vault items awaiting approval
│ ├── approved/ # approved vault items
│ ├── fired/ # executed vault items
│ └── rejected/ # rejected vault items
├── knowledge/ # shared agent knowledge and best practices
├── evidence/ # engagement data, experiment results
├── portfolio.md # addressables + observables
├── prerequisites.md # dependency graph
└── RESOURCES.md # accounts, tokens (refs), infra inventory
\`\`\`
> **Note:** Journal directories (journal/planner/ and journal/supervisor/) have been removed from the ops repo. Agent journals are now stored in each agent's .profile repo on Forgejo.
## Branch protection
- \`main\`: 2 reviewers required for vault items
- Journal/evidence commits may use lighter rules
OPSEOF
seeded=true
fi
# Create stub files if they don't exist
[ -f "${ops_root}/portfolio.md" ] || { echo "# Portfolio" > "${ops_root}/portfolio.md"; seeded=true; }
[ -f "${ops_root}/prerequisites.md" ] || { echo "# Prerequisite Tree" > "${ops_root}/prerequisites.md"; seeded=true; }
[ -f "${ops_root}/RESOURCES.md" ] || { echo "# Resources" > "${ops_root}/RESOURCES.md"; seeded=true; }
# Commit and push seed content
if [ "$seeded" = true ] && [ -d "${ops_root}/.git" ]; then
# Auto-configure repo-local git identity if missing (#778)
if [ -z "$(git -C "$ops_root" config user.name 2>/dev/null)" ]; then
git -C "$ops_root" config user.name "disinto-admin"
fi
if [ -z "$(git -C "$ops_root" config user.email 2>/dev/null)" ]; then
git -C "$ops_root" config user.email "disinto-admin@localhost"
fi
git -C "$ops_root" add -A
if ! git -C "$ops_root" diff --cached --quiet 2>/dev/null; then
git -C "$ops_root" commit -m "chore: seed ops repo structure" -q
# Push if remote exists
if git -C "$ops_root" remote get-url origin >/dev/null 2>&1; then
if git -C "$ops_root" push origin "${primary_branch}" -q 2>/dev/null; then
echo "Seeded: ops repo with initial structure"
else
echo "Warning: failed to push seed content to ops repo" >&2
fi
fi
fi
fi
# Export resolved slug for the caller to write back to the project TOML
_ACTUAL_OPS_SLUG="${actual_ops_slug}"
}
# ops repo setup is now in lib/ops-setup.sh
# Push local clone to the Forgejo remote.
push_to_forge() {

225
lib/ops-setup.sh Normal file
View file

@ -0,0 +1,225 @@
#!/usr/bin/env bash
# ops-setup.sh — Setup ops repository (disinto-ops)
#
# Source from bin/disinto:
# source "$(dirname "$0")/../lib/ops-setup.sh"
#
# Required globals: FORGE_URL, FORGE_TOKEN, FACTORY_ROOT
# Optional: admin_token (falls back to FORGE_TOKEN for admin operations)
#
# Functions:
# setup_ops_repo <forge_url> <ops_slug> <ops_root> [primary_branch]
# - Create ops repo on Forgejo if it doesn't exist
# - Configure bot collaborators with appropriate permissions
# - Clone or initialize ops repo locally
# - Seed directory structure (vault, knowledge, evidence)
# - Export _ACTUAL_OPS_SLUG for caller to use
#
# Globals modified:
# _ACTUAL_OPS_SLUG - resolved ops repo slug after function completes
set -euo pipefail
setup_ops_repo() {
local forge_url="$1" ops_slug="$2" ops_root="$3" primary_branch="${4:-main}"
local org_name="${ops_slug%%/*}"
local ops_name="${ops_slug##*/}"
echo ""
echo "── Ops repo setup ─────────────────────────────────────"
# Determine the actual ops repo location by searching across possible namespaces
# This handles cases where the repo was created under a different namespace
# due to past bugs (e.g., dev-bot/disinto-ops instead of disinto-admin/disinto-ops)
local actual_ops_slug=""
local -a possible_namespaces=( "$org_name" "dev-bot" "disinto-admin" )
local http_code
for ns in "${possible_namespaces[@]}"; do
slug="${ns}/${ops_name}"
if curl -sf --max-time 5 \
-H "Authorization: token ${FORGE_TOKEN}" \
"${forge_url}/api/v1/repos/${slug}" >/dev/null 2>&1; then
actual_ops_slug="$slug"
echo "Ops repo: ${slug} (found at ${slug})"
break
fi
done
# If not found, try to create it in the configured namespace
if [ -z "$actual_ops_slug" ]; then
echo "Creating ops repo in namespace: ${org_name}"
# Create org if it doesn't exist
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
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\":\"${ops_name}\",\"auto_init\":true,\"default_branch\":\"${primary_branch}\",\"description\":\"Operational data for ${org_name}/${ops_name%-ops}\"}" >/dev/null 2>&1; then
actual_ops_slug="${org_name}/${ops_name}"
echo "Ops repo: ${actual_ops_slug} created on Forgejo"
else
# Fallback: use admin API to create repo under the target namespace
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST \
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
-H "Content-Type: application/json" \
"${forge_url}/api/v1/admin/users/${org_name}/repos" \
-d "{\"name\":\"${ops_name}\",\"auto_init\":true,\"default_branch\":\"${primary_branch}\",\"description\":\"Operational data for ${org_name}/${ops_name%-ops}\"}" 2>/dev/null || echo "0")
if [ "$http_code" = "201" ]; then
actual_ops_slug="${org_name}/${ops_name}"
echo "Ops repo: ${actual_ops_slug} created on Forgejo (via admin API)"
else
echo "Error: failed to create ops repo '${org_name}/${ops_name}' (HTTP ${http_code})" >&2
return 1
fi
fi
fi
# Configure collaborators on the ops repo
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"
)
# Add all bot users as collaborators with appropriate permissions
# vault branch protection (#77) requires:
# - Admin-only merge to main (enforced by admin_enforced: true)
# - Bots can push branches and create PRs, but cannot merge
for bot_user in "${!bot_permissions[@]}"; do
bot_perm="${bot_permissions[$bot_user]}"
if curl -sf -X PUT \
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
-H "Content-Type: application/json" \
"${forge_url}/api/v1/repos/${actual_ops_slug}/collaborators/${bot_user}" \
-d "{\"permission\":\"${bot_perm}\"}" >/dev/null 2>&1; then
echo " + ${bot_user} = ${bot_perm} collaborator"
else
echo " ! ${bot_user} = ${bot_perm} (already set or failed)"
fi
done
# Add disinto-admin as admin collaborator
if curl -sf -X PUT \
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
-H "Content-Type: application/json" \
"${forge_url}/api/v1/repos/${actual_ops_slug}/collaborators/disinto-admin" \
-d '{"permission":"admin"}' >/dev/null 2>&1; then
echo " + disinto-admin = admin collaborator"
else
echo " ! disinto-admin = admin (already set or failed)"
fi
# Clone ops repo locally if not present
if [ ! -d "${ops_root}/.git" ]; then
local auth_url
auth_url=$(printf '%s' "$forge_url" | sed "s|://|://dev-bot:${FORGE_TOKEN}@|")
local clone_url="${auth_url}/${actual_ops_slug}.git"
echo "Cloning: ops repo -> ${ops_root}"
if git clone --quiet "$clone_url" "$ops_root" 2>/dev/null; then
echo "Ops repo: ${actual_ops_slug} cloned successfully"
else
echo "Initializing: ops repo at ${ops_root}"
mkdir -p "$ops_root"
git -C "$ops_root" init --initial-branch="${primary_branch}" -q
# Set remote to the actual ops repo location
git -C "$ops_root" remote add origin "${forge_url}/${actual_ops_slug}.git"
echo "Ops repo: ${actual_ops_slug} initialized locally"
fi
else
echo "Ops repo: ${ops_root} (already exists locally)"
# Verify remote is correct
local current_remote
current_remote=$(git -C "$ops_root" remote get-url origin 2>/dev/null || true)
local expected_remote="${forge_url}/${actual_ops_slug}.git"
if [ -n "$current_remote" ] && [ "$current_remote" != "$expected_remote" ]; then
echo " Fixing: remote URL from ${current_remote} to ${expected_remote}"
git -C "$ops_root" remote set-url origin "$expected_remote"
fi
fi
# Seed directory structure
local seeded=false
mkdir -p "${ops_root}/vault/pending"
mkdir -p "${ops_root}/vault/approved"
mkdir -p "${ops_root}/vault/fired"
mkdir -p "${ops_root}/vault/rejected"
mkdir -p "${ops_root}/knowledge"
mkdir -p "${ops_root}/evidence/engagement"
if [ ! -f "${ops_root}/README.md" ]; then
cat > "${ops_root}/README.md" <<OPSEOF
# ${ops_name}
Operational data for the ${ops_name%-ops} project.
## Structure
\`\`\`
${ops_name}/
├── vault/
│ ├── pending/ # vault items awaiting approval
│ ├── approved/ # approved vault items
│ ├── fired/ # executed vault items
│ └── rejected/ # rejected vault items
├── knowledge/ # shared agent knowledge and best practices
├── evidence/ # engagement data, experiment results
├── portfolio.md # addressables + observables
├── prerequisites.md # dependency graph
└── RESOURCES.md # accounts, tokens (refs), infra inventory
\`\`\`
> **Note:** Journal directories (journal/planner/ and journal/supervisor/) have been removed from the ops repo. Agent journals are now stored in each agent's .profile repo on Forgejo.
## Branch protection
- \`main\`: 2 reviewers required for vault items
- Journal/evidence commits may use lighter rules
OPSEOF
seeded=true
fi
# Create stub files if they don't exist
[ -f "${ops_root}/portfolio.md" ] || { echo "# Portfolio" > "${ops_root}/portfolio.md"; seeded=true; }
[ -f "${ops_root}/prerequisites.md" ] || { echo "# Prerequisite Tree" > "${ops_root}/prerequisites.md"; seeded=true; }
[ -f "${ops_root}/RESOURCES.md" ] || { echo "# Resources" > "${ops_root}/RESOURCES.md"; seeded=true; }
# Commit and push seed content
if [ "$seeded" = true ] && [ -d "${ops_root}/.git" ]; then
# Auto-configure repo-local git identity if missing (#778)
if [ -z "$(git -C "$ops_root" config user.name 2>/dev/null)" ]; then
git -C "$ops_root" config user.name "disinto-admin"
fi
if [ -z "$(git -C "$ops_root" config user.email 2>/dev/null)" ]; then
git -C "$ops_root" config user.email "disinto-admin@localhost"
fi
git -C "$ops_root" add -A
if ! git -C "$ops_root" diff --cached --quiet 2>/dev/null; then
git -C "$ops_root" commit -m "chore: seed ops repo structure" -q
# Push if remote exists
if git -C "$ops_root" remote get-url origin >/dev/null 2>&1; then
if git -C "$ops_root" push origin "${primary_branch}" -q 2>/dev/null; then
echo "Seeded: ops repo with initial structure"
else
echo "Warning: failed to push seed content to ops repo" >&2
fi
fi
fi
fi
# Export resolved slug for the caller to write back to the project TOML
_ACTUAL_OPS_SLUG="${actual_ops_slug}"
}