fix: fix: disinto init must be fully idempotent — safe to re-run on existing factory (#239)
This commit is contained in:
parent
dcf348e486
commit
979e1210b4
3 changed files with 253 additions and 73 deletions
268
bin/disinto
268
bin/disinto
|
|
@ -177,6 +177,12 @@ generate_compose() {
|
||||||
local forge_port="${1:-3000}"
|
local forge_port="${1:-3000}"
|
||||||
local compose_file="${FACTORY_ROOT}/docker-compose.yml"
|
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'
|
cat > "$compose_file" <<'COMPOSEEOF'
|
||||||
# docker-compose.yml — generated by disinto init
|
# docker-compose.yml — generated by disinto init
|
||||||
# Brings up Forgejo, Woodpecker, and the agent runtime.
|
# Brings up Forgejo, Woodpecker, and the agent runtime.
|
||||||
|
|
@ -818,9 +824,15 @@ setup_forge() {
|
||||||
bot_pass="bot-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)"
|
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]}"
|
token_var="${bot_token_vars[$bot_user]}"
|
||||||
|
|
||||||
if ! curl -sf --max-time 5 \
|
# Check if bot user exists
|
||||||
|
local user_exists=false
|
||||||
|
if curl -sf --max-time 5 \
|
||||||
-H "Authorization: token ${admin_token}" \
|
-H "Authorization: token ${admin_token}" \
|
||||||
"${forge_url}/api/v1/users/${bot_user}" >/dev/null 2>&1; then
|
"${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}"
|
echo "Creating bot user: ${bot_user}"
|
||||||
local create_output
|
local create_output
|
||||||
if ! create_output=$(_forgejo_exec forgejo admin user create \
|
if ! create_output=$(_forgejo_exec forgejo admin user create \
|
||||||
|
|
@ -846,10 +858,38 @@ setup_forge() {
|
||||||
echo "Error: bot user '${bot_user}' not found after creation" >&2
|
echo "Error: bot user '${bot_user}' not found after creation" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
fi
|
||||||
|
|
||||||
# Generate token via API (basic auth as the bot user — Forgejo requires
|
# Generate token via API (basic auth as the bot user — Forgejo requires
|
||||||
# basic auth on POST /users/{username}/tokens, token auth is rejected)
|
# basic auth on POST /users/{username}/tokens, token auth is rejected)
|
||||||
|
# First, try to delete existing tokens to avoid name collision
|
||||||
|
local existing_token_ids
|
||||||
|
existing_token_ids=$(curl -sf \
|
||||||
|
-H "Authorization: token ${admin_token}" \
|
||||||
|
"${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 \
|
||||||
|
-H "Authorization: token ${admin_token}" \
|
||||||
|
"${forge_url}/api/v1/users/${bot_user}/tokens/${tid}" >/dev/null 2>&1 || true
|
||||||
|
done <<< "$existing_token_ids"
|
||||||
|
fi
|
||||||
|
|
||||||
token=$(curl -sf -X POST \
|
token=$(curl -sf -X POST \
|
||||||
-u "${bot_user}:${bot_pass}" \
|
-u "${bot_user}:${bot_pass}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
|
@ -857,16 +897,6 @@ setup_forge() {
|
||||||
-d "{\"name\":\"disinto-${bot_user}-token\",\"scopes\":[\"all\"]}" 2>/dev/null \
|
-d "{\"name\":\"disinto-${bot_user}-token\",\"scopes\":[\"all\"]}" 2>/dev/null \
|
||||||
| jq -r '.sha1 // empty') || token=""
|
| jq -r '.sha1 // empty') || token=""
|
||||||
|
|
||||||
if [ -z "$token" ]; then
|
|
||||||
# Token name collision — create with timestamp suffix
|
|
||||||
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}-$(date +%s)\",\"scopes\":[\"all\"]}" 2>/dev/null \
|
|
||||||
| jq -r '.sha1 // empty') || token=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$token" ]; then
|
if [ -z "$token" ]; then
|
||||||
echo "Error: failed to create API token for '${bot_user}'" >&2
|
echo "Error: failed to create API token for '${bot_user}'" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -879,7 +909,7 @@ setup_forge() {
|
||||||
printf '%s=%s\n' "$token_var" "$token" >> "$env_file"
|
printf '%s=%s\n' "$token_var" "$token" >> "$env_file"
|
||||||
fi
|
fi
|
||||||
export "${token_var}=${token}"
|
export "${token_var}=${token}"
|
||||||
echo " ${bot_user} token saved (${token_var})"
|
echo " ${bot_user} token generated and saved (${token_var})"
|
||||||
|
|
||||||
# Backwards-compat aliases for dev-bot and review-bot
|
# Backwards-compat aliases for dev-bot and review-bot
|
||||||
if [ "$bot_user" = "dev-bot" ]; then
|
if [ "$bot_user" = "dev-bot" ]; then
|
||||||
|
|
@ -995,76 +1025,125 @@ setup_ops_repo() {
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Ops repo setup ─────────────────────────────────────"
|
echo "── Ops repo setup ─────────────────────────────────────"
|
||||||
|
|
||||||
# Check if ops repo already exists on Forgejo
|
# Determine the actual ops repo location by searching across possible namespaces
|
||||||
if curl -sf --max-time 5 \
|
# This handles cases where the repo was created under a different namespace
|
||||||
-H "Authorization: token ${FORGE_TOKEN}" \
|
# due to past bugs (e.g., dev-bot/disinto-ops instead of disinto-admin/disinto-ops)
|
||||||
"${forge_url}/api/v1/repos/${ops_slug}" >/dev/null 2>&1; then
|
local actual_ops_slug=""
|
||||||
echo "Ops repo: ${ops_slug} (already exists on Forgejo)"
|
local -a possible_namespaces=( "$org_name" "dev-bot" "disinto-admin" )
|
||||||
else
|
local http_code
|
||||||
# Create ops repo under org (or human user if org creation failed)
|
|
||||||
if ! curl -sf -X POST \
|
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 "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${forge_url}/api/v1/orgs/${org_name}/repos" \
|
"${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
|
-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
|
||||||
# Fallback: use admin API to create repo under the target namespace.
|
actual_ops_slug="${org_name}/${ops_name}"
|
||||||
# POST /api/v1/users/{username}/repos creates under the authenticated user,
|
echo "Ops repo: ${actual_ops_slug} created on Forgejo"
|
||||||
# not under {username}. The admin API POST /api/v1/admin/users/{username}/repos
|
else
|
||||||
# explicitly creates in the target user's namespace regardless of who is authed.
|
# Fallback: use admin API to create repo under the target namespace
|
||||||
curl -sf -X POST \
|
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||||
|
-X POST \
|
||||||
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${forge_url}/api/v1/admin/users/${org_name}/repos" \
|
"${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}\"}" >/dev/null 2>&1 || true
|
-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 '${actual_ops_slug}' (HTTP ${http_code})" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Add all bot users as collaborators with appropriate permissions
|
# Configure collaborators on the ops repo
|
||||||
# vault branch protection (#77) requires:
|
local bot_user bot_perm
|
||||||
# - Admin-only merge to main (enforced by admin_enforced: true)
|
declare -A bot_permissions=(
|
||||||
# - Bots can push branches and create PRs, but cannot merge
|
[dev-bot]="write"
|
||||||
local bot_user bot_perm
|
[review-bot]="read"
|
||||||
declare -A bot_permissions=(
|
[planner-bot]="write"
|
||||||
[dev-bot]="write"
|
[gardener-bot]="write"
|
||||||
[review-bot]="read"
|
[vault-bot]="write"
|
||||||
[planner-bot]="write"
|
[supervisor-bot]="read"
|
||||||
[gardener-bot]="write"
|
[predictor-bot]="read"
|
||||||
[vault-bot]="write"
|
[architect-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/${ops_slug}/collaborators/${bot_user}" \
|
|
||||||
-d "{\"permission\":\"${bot_perm}\"}" >/dev/null 2>&1 || true
|
|
||||||
done
|
|
||||||
|
|
||||||
# Add disinto-admin as admin collaborator
|
# Add all bot users as collaborators with appropriate permissions
|
||||||
curl -sf -X PUT \
|
# 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 "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${forge_url}/api/v1/repos/${ops_slug}/collaborators/disinto-admin" \
|
"${forge_url}/api/v1/repos/${actual_ops_slug}/collaborators/${bot_user}" \
|
||||||
-d '{"permission":"admin"}' >/dev/null 2>&1 || true
|
-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
|
||||||
|
|
||||||
echo "Ops repo: ${ops_slug} created on Forgejo"
|
# 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
|
fi
|
||||||
|
|
||||||
# Clone ops repo locally if not present
|
# Clone ops repo locally if not present
|
||||||
if [ ! -d "${ops_root}/.git" ]; then
|
if [ ! -d "${ops_root}/.git" ]; then
|
||||||
local auth_url
|
local auth_url
|
||||||
auth_url=$(printf '%s' "$forge_url" | sed "s|://|://dev-bot:${FORGE_TOKEN}@|")
|
auth_url=$(printf '%s' "$forge_url" | sed "s|://|://dev-bot:${FORGE_TOKEN}@|")
|
||||||
local clone_url="${auth_url}/${ops_slug}.git"
|
local clone_url="${auth_url}/${actual_ops_slug}.git"
|
||||||
echo "Cloning: ops repo -> ${ops_root}"
|
echo "Cloning: ops repo -> ${ops_root}"
|
||||||
git clone --quiet "$clone_url" "$ops_root" 2>/dev/null || {
|
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}"
|
echo "Initializing: ops repo at ${ops_root}"
|
||||||
mkdir -p "$ops_root"
|
mkdir -p "$ops_root"
|
||||||
git -C "$ops_root" init --initial-branch="${primary_branch}" -q
|
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
|
else
|
||||||
echo "Ops repo: ${ops_root} (already exists locally)"
|
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
|
fi
|
||||||
|
|
||||||
# Seed directory structure
|
# Seed directory structure
|
||||||
|
|
@ -1128,10 +1207,13 @@ OPSEOF
|
||||||
git -C "$ops_root" commit -m "chore: seed ops repo structure" -q
|
git -C "$ops_root" commit -m "chore: seed ops repo structure" -q
|
||||||
# Push if remote exists
|
# Push if remote exists
|
||||||
if git -C "$ops_root" remote get-url origin >/dev/null 2>&1; then
|
if git -C "$ops_root" remote get-url origin >/dev/null 2>&1; then
|
||||||
git -C "$ops_root" push origin "${primary_branch}" -q 2>/dev/null || true
|
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
|
fi
|
||||||
echo "Seeded: ops repo with initial structure"
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1386,9 +1468,11 @@ create_labels() {
|
||||||
| grep -o '"name":"[^"]*"' | cut -d'"' -f4) || existing=""
|
| grep -o '"name":"[^"]*"' | cut -d'"' -f4) || existing=""
|
||||||
|
|
||||||
local name color
|
local name color
|
||||||
|
local created=0 skipped=0 failed=0
|
||||||
for name in backlog in-progress blocked tech-debt underspecified vision action bug-report prediction/unreviewed prediction/dismissed prediction/actioned; do
|
for name in backlog in-progress blocked tech-debt underspecified vision action bug-report prediction/unreviewed prediction/dismissed prediction/actioned; do
|
||||||
if echo "$existing" | grep -qx "$name"; then
|
if echo "$existing" | grep -qx "$name"; then
|
||||||
echo " . ${name} (already exists)"
|
echo " . ${name} (already exists)"
|
||||||
|
skipped=$((skipped + 1))
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
color="${labels[$name]}"
|
color="${labels[$name]}"
|
||||||
|
|
@ -1397,11 +1481,15 @@ create_labels() {
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${api}/labels" \
|
"${api}/labels" \
|
||||||
-d "{\"name\":\"${name}\",\"color\":\"${color}\"}" >/dev/null 2>&1; then
|
-d "{\"name\":\"${name}\",\"color\":\"${color}\"}" >/dev/null 2>&1; then
|
||||||
echo " + ${name}"
|
echo " + ${name} (created)"
|
||||||
|
created=$((created + 1))
|
||||||
else
|
else
|
||||||
echo " ! ${name} (failed to create)"
|
echo " ! ${name} (failed to create)"
|
||||||
|
failed=$((failed + 1))
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
echo "Labels: ${created} created, ${skipped} skipped, ${failed} failed"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generate a minimal VISION.md template in the target project.
|
# Generate a minimal VISION.md template in the target project.
|
||||||
|
|
@ -1503,6 +1591,14 @@ install_cron() {
|
||||||
echo "$cron_block"
|
echo "$cron_block"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Check if cron entries already exist
|
||||||
|
local current_crontab
|
||||||
|
current_crontab=$(crontab -l 2>/dev/null || true)
|
||||||
|
if echo "$current_crontab" | grep -q "# disinto: ${name}"; then
|
||||||
|
echo "Cron: skipped (entries for ${name} already installed)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$auto_yes" = false ] && [ -t 0 ]; then
|
if [ "$auto_yes" = false ] && [ -t 0 ]; then
|
||||||
read -rp "Install these cron entries? [y/N] " confirm
|
read -rp "Install these cron entries? [y/N] " confirm
|
||||||
if [[ ! "$confirm" =~ ^[Yy] ]]; then
|
if [[ ! "$confirm" =~ ^[Yy] ]]; then
|
||||||
|
|
@ -1512,8 +1608,12 @@ install_cron() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Append to existing crontab
|
# Append to existing crontab
|
||||||
{ crontab -l 2>/dev/null || true; printf '%s\n' "$cron_block"; } | crontab -
|
if { crontab -l 2>/dev/null || true; printf '%s\n' "$cron_block"; } | crontab -; then
|
||||||
echo "Cron entries installed"
|
echo "Cron entries installed for ${name}"
|
||||||
|
else
|
||||||
|
echo "Error: failed to install cron entries" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Set up Woodpecker CI to use Forgejo as its forge backend.
|
# Set up Woodpecker CI to use Forgejo as its forge backend.
|
||||||
|
|
@ -2091,17 +2191,36 @@ p.write_text(text)
|
||||||
if [ -n "${MIRROR_NAMES:-}" ]; then
|
if [ -n "${MIRROR_NAMES:-}" ]; then
|
||||||
echo "Mirrors: setting up remotes"
|
echo "Mirrors: setting up remotes"
|
||||||
local mname murl
|
local mname murl
|
||||||
|
local mirrors_ok=true
|
||||||
for mname in $MIRROR_NAMES; do
|
for mname in $MIRROR_NAMES; do
|
||||||
murl=$(eval "echo \"\$MIRROR_$(echo "$mname" | tr '[:lower:]' '[:upper:]')\"") || true
|
murl=$(eval "echo \"\$MIRROR_$(echo "$mname" | tr '[:lower:]' '[:upper:]')\"") || true
|
||||||
[ -z "$murl" ] && continue
|
[ -z "$murl" ] && continue
|
||||||
git -C "$repo_root" remote add "$mname" "$murl" 2>/dev/null \
|
if git -C "$repo_root" remote get-url "$mname" >/dev/null 2>&1; then
|
||||||
|| git -C "$repo_root" remote set-url "$mname" "$murl" 2>/dev/null || true
|
if git -C "$repo_root" remote set-url "$mname" "$murl"; then
|
||||||
echo " + ${mname} -> ${murl}"
|
echo " + ${mname} -> ${murl} (updated)"
|
||||||
|
else
|
||||||
|
echo " ! ${mname} -> ${murl} (failed to update URL)"
|
||||||
|
mirrors_ok=false
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if git -C "$repo_root" remote add "$mname" "$murl"; then
|
||||||
|
echo " + ${mname} -> ${murl} (added)"
|
||||||
|
else
|
||||||
|
echo " ! ${mname} -> ${murl} (failed to add remote)"
|
||||||
|
mirrors_ok=false
|
||||||
|
fi
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
# Initial sync: push current primary branch to mirrors
|
# Initial sync: push current primary branch to mirrors
|
||||||
source "${FACTORY_ROOT}/lib/mirrors.sh"
|
if [ "$mirrors_ok" = true ]; then
|
||||||
export PROJECT_REPO_ROOT="$repo_root"
|
source "${FACTORY_ROOT}/lib/mirrors.sh"
|
||||||
mirror_push
|
export PROJECT_REPO_ROOT="$repo_root"
|
||||||
|
if mirror_push; then
|
||||||
|
echo "Mirrors: initial sync complete"
|
||||||
|
else
|
||||||
|
echo "Warning: mirror push failed" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Encrypt secrets if SOPS + age are available
|
# Encrypt secrets if SOPS + age are available
|
||||||
|
|
@ -2140,9 +2259,16 @@ p.write_text(text)
|
||||||
# Activate default agents (zero-cost when idle — they only invoke Claude
|
# Activate default agents (zero-cost when idle — they only invoke Claude
|
||||||
# when there is actual work, so an empty project burns no LLM tokens)
|
# when there is actual work, so an empty project burns no LLM tokens)
|
||||||
mkdir -p "${FACTORY_ROOT}/state"
|
mkdir -p "${FACTORY_ROOT}/state"
|
||||||
touch "${FACTORY_ROOT}/state/.dev-active"
|
|
||||||
touch "${FACTORY_ROOT}/state/.reviewer-active"
|
# State files are idempotent — create if missing, skip if present
|
||||||
touch "${FACTORY_ROOT}/state/.gardener-active"
|
for state_file in ".dev-active" ".reviewer-active" ".gardener-active"; do
|
||||||
|
if [ -f "${FACTORY_ROOT}/state/${state_file}" ]; then
|
||||||
|
echo "State: ${state_file} (already active)"
|
||||||
|
else
|
||||||
|
touch "${FACTORY_ROOT}/state/${state_file}"
|
||||||
|
echo "State: ${state_file} (created)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Done. Project ${project_name} is ready."
|
echo "Done. Project ${project_name} is ready."
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Mock Forgejo API server for CI smoke tests.
|
"""Mock Forgejo API server for CI smoke tests.
|
||||||
|
|
||||||
Implements 15 Forgejo API endpoints that disinto init calls.
|
Implements 16 Forgejo API endpoints that disinto init calls.
|
||||||
State stored in-memory (dicts), responds instantly.
|
State stored in-memory (dicts), responds instantly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -149,6 +149,7 @@ class ForgejoHandler(BaseHTTPRequestHandler):
|
||||||
# Admin patterns
|
# Admin patterns
|
||||||
(r"^admin/users$", f"handle_{method}_admin_users"),
|
(r"^admin/users$", f"handle_{method}_admin_users"),
|
||||||
(r"^admin/users/([^/]+)$", f"handle_{method}_admin_users_username"),
|
(r"^admin/users/([^/]+)$", f"handle_{method}_admin_users_username"),
|
||||||
|
(r"^admin/users/([^/]+)/repos$", f"handle_{method}_admin_users_username_repos"),
|
||||||
# Org patterns
|
# Org patterns
|
||||||
(r"^orgs$", f"handle_{method}_orgs"),
|
(r"^orgs$", f"handle_{method}_orgs"),
|
||||||
]
|
]
|
||||||
|
|
@ -294,7 +295,10 @@ class ForgejoHandler(BaseHTTPRequestHandler):
|
||||||
|
|
||||||
def handle_GET_users_username_tokens(self, query):
|
def handle_GET_users_username_tokens(self, query):
|
||||||
"""GET /api/v1/users/{username}/tokens"""
|
"""GET /api/v1/users/{username}/tokens"""
|
||||||
|
# Support both token auth (for listing own tokens) and basic auth (for admin listing)
|
||||||
username = require_token(self)
|
username = require_token(self)
|
||||||
|
if not username:
|
||||||
|
username = require_basic_auth(self)
|
||||||
if not username:
|
if not username:
|
||||||
json_response(self, 401, {"message": "invalid authentication"})
|
json_response(self, 401, {"message": "invalid authentication"})
|
||||||
return
|
return
|
||||||
|
|
@ -460,6 +464,55 @@ class ForgejoHandler(BaseHTTPRequestHandler):
|
||||||
state["repos"][key] = repo
|
state["repos"][key] = repo
|
||||||
json_response(self, 201, repo)
|
json_response(self, 201, repo)
|
||||||
|
|
||||||
|
def handle_POST_admin_users_username_repos(self, query):
|
||||||
|
"""POST /api/v1/admin/users/{username}/repos
|
||||||
|
Admin API to create a repo under a specific user namespace.
|
||||||
|
This allows creating repos in any user's namespace when authenticated as admin.
|
||||||
|
"""
|
||||||
|
require_token(self)
|
||||||
|
|
||||||
|
parts = self.path.split("/")
|
||||||
|
if len(parts) >= 6:
|
||||||
|
target_user = parts[4]
|
||||||
|
else:
|
||||||
|
json_response(self, 400, {"message": "username required"})
|
||||||
|
return
|
||||||
|
|
||||||
|
if target_user not in state["users"]:
|
||||||
|
json_response(self, 404, {"message": "user not found"})
|
||||||
|
return
|
||||||
|
|
||||||
|
content_length = int(self.headers.get("Content-Length", 0))
|
||||||
|
body = self.rfile.read(content_length).decode("utf-8")
|
||||||
|
data = json.loads(body) if body else {}
|
||||||
|
|
||||||
|
repo_name = data.get("name")
|
||||||
|
if not repo_name:
|
||||||
|
json_response(self, 400, {"message": "name is required"})
|
||||||
|
return
|
||||||
|
|
||||||
|
repo_id = next_ids["repos"]
|
||||||
|
next_ids["repos"] += 1
|
||||||
|
|
||||||
|
key = f"{target_user}/{repo_name}"
|
||||||
|
repo = {
|
||||||
|
"id": repo_id,
|
||||||
|
"full_name": key,
|
||||||
|
"name": repo_name,
|
||||||
|
"owner": {"id": state["users"][target_user]["id"], "login": target_user},
|
||||||
|
"empty": not data.get("auto_init", False),
|
||||||
|
"default_branch": data.get("default_branch", "main"),
|
||||||
|
"description": data.get("description", ""),
|
||||||
|
"private": data.get("private", False),
|
||||||
|
"html_url": f"https://example.com/{key}",
|
||||||
|
"ssh_url": f"git@example.com:{key}.git",
|
||||||
|
"clone_url": f"https://example.com/{key}.git",
|
||||||
|
"created_at": "2026-04-01T00:00:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
state["repos"][key] = repo
|
||||||
|
json_response(self, 201, repo)
|
||||||
|
|
||||||
def handle_POST_user_repos(self, query):
|
def handle_POST_user_repos(self, query):
|
||||||
"""POST /api/v1/user/repos"""
|
"""POST /api/v1/user/repos"""
|
||||||
require_token(self)
|
require_token(self)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
FORGE_URL="${FORGE_URL:-http://localhost:3000}"
|
# Always use localhost for mock Forgejo (in case FORGE_URL is set from docker-compose)
|
||||||
|
export FORGE_URL="http://localhost:3000"
|
||||||
MOCK_BIN="/tmp/smoke-mock-bin"
|
MOCK_BIN="/tmp/smoke-mock-bin"
|
||||||
TEST_SLUG="smoke-org/smoke-repo"
|
TEST_SLUG="smoke-org/smoke-repo"
|
||||||
FAILED=0
|
FAILED=0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue