fix: fix: disinto init must be fully idempotent — safe to re-run on existing factory (#239)
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

This commit is contained in:
Agent 2026-04-05 21:15:25 +00:00
parent dcf348e486
commit 979e1210b4
3 changed files with 253 additions and 73 deletions

View file

@ -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."

View file

@ -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)

View file

@ -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