Merge pull request 'fix: fix: rewrite smoke-init.sh for mock Forgejo + restore pipeline (#143)' (#147) from fix/issue-143 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
This commit is contained in:
commit
fe4ab7d447
4 changed files with 194 additions and 181 deletions
17
.woodpecker/smoke-init.yml
Normal file
17
.woodpecker/smoke-init.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
path:
|
||||||
|
- "bin/disinto"
|
||||||
|
- "lib/load-project.sh"
|
||||||
|
- "lib/env.sh"
|
||||||
|
- "tests/**"
|
||||||
|
- ".woodpecker/smoke-init.yml"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: smoke-init
|
||||||
|
image: python:3-alpine
|
||||||
|
commands:
|
||||||
|
- apk add --no-cache bash curl jq git coreutils
|
||||||
|
- python3 tests/mock-forgejo.py &
|
||||||
|
- sleep 2
|
||||||
|
- bash tests/smoke-init.sh
|
||||||
|
|
@ -784,6 +784,7 @@ setup_forge() {
|
||||||
[vault-bot]="FORGE_VAULT_TOKEN"
|
[vault-bot]="FORGE_VAULT_TOKEN"
|
||||||
[supervisor-bot]="FORGE_SUPERVISOR_TOKEN"
|
[supervisor-bot]="FORGE_SUPERVISOR_TOKEN"
|
||||||
[predictor-bot]="FORGE_PREDICTOR_TOKEN"
|
[predictor-bot]="FORGE_PREDICTOR_TOKEN"
|
||||||
|
[architect-bot]="FORGE_ARCHITECT_TOKEN"
|
||||||
)
|
)
|
||||||
|
|
||||||
local bot_user bot_pass token token_var
|
local bot_user bot_pass token token_var
|
||||||
|
|
@ -1919,8 +1920,10 @@ p.write_text(text)
|
||||||
echo "Repo: ${repo_root} (existing clone)"
|
echo "Repo: ${repo_root} (existing clone)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Push to local Forgejo
|
# Push to local Forgejo (skip if SKIP_PUSH is set)
|
||||||
push_to_forge "$repo_root" "$forge_url" "$forge_repo"
|
if [ "${SKIP_PUSH:-false}" = "false" ]; then
|
||||||
|
push_to_forge "$repo_root" "$forge_url" "$forge_repo"
|
||||||
|
fi
|
||||||
|
|
||||||
# Detect primary branch
|
# Detect primary branch
|
||||||
if [ -z "$branch" ]; then
|
if [ -z "$branch" ]; then
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ class ForgejoHandler(BaseHTTPRequestHandler):
|
||||||
# Users patterns
|
# Users patterns
|
||||||
(r"^users/([^/]+)$", f"handle_{method}_users_username"),
|
(r"^users/([^/]+)$", f"handle_{method}_users_username"),
|
||||||
(r"^users/([^/]+)/tokens$", f"handle_{method}_users_username_tokens"),
|
(r"^users/([^/]+)/tokens$", f"handle_{method}_users_username_tokens"),
|
||||||
|
(r"^users/([^/]+)/repos$", f"handle_{method}_users_username_repos"),
|
||||||
# Repos patterns
|
# Repos patterns
|
||||||
(r"^repos/([^/]+)/([^/]+)$", f"handle_{method}_repos_owner_repo"),
|
(r"^repos/([^/]+)/([^/]+)$", f"handle_{method}_repos_owner_repo"),
|
||||||
(r"^repos/([^/]+)/([^/]+)/labels$", f"handle_{method}_repos_owner_repo_labels"),
|
(r"^repos/([^/]+)/([^/]+)/labels$", f"handle_{method}_repos_owner_repo_labels"),
|
||||||
|
|
@ -192,6 +193,27 @@ class ForgejoHandler(BaseHTTPRequestHandler):
|
||||||
else:
|
else:
|
||||||
json_response(self, 404, {"message": "user does not exist"})
|
json_response(self, 404, {"message": "user does not exist"})
|
||||||
|
|
||||||
|
def handle_GET_users_username_repos(self, query):
|
||||||
|
"""GET /api/v1/users/{username}/repos"""
|
||||||
|
if not require_token(self):
|
||||||
|
json_response(self, 401, {"message": "invalid authentication"})
|
||||||
|
return
|
||||||
|
|
||||||
|
parts = self.path.split("/")
|
||||||
|
if len(parts) >= 5:
|
||||||
|
username = parts[4]
|
||||||
|
else:
|
||||||
|
json_response(self, 404, {"message": "user not found"})
|
||||||
|
return
|
||||||
|
|
||||||
|
if username not in state["users"]:
|
||||||
|
json_response(self, 404, {"message": "user not found"})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Return repos owned by this user
|
||||||
|
user_repos = [r for r in state["repos"].values() if r["owner"]["login"] == username]
|
||||||
|
json_response(self, 200, user_repos)
|
||||||
|
|
||||||
def handle_GET_repos_owner_repo(self, query):
|
def handle_GET_repos_owner_repo(self, query):
|
||||||
"""GET /api/v1/repos/{owner}/{repo}"""
|
"""GET /api/v1/repos/{owner}/{repo}"""
|
||||||
parts = self.path.split("/")
|
parts = self.path.split("/")
|
||||||
|
|
@ -270,6 +292,17 @@ class ForgejoHandler(BaseHTTPRequestHandler):
|
||||||
state["users"][username] = user
|
state["users"][username] = user
|
||||||
json_response(self, 201, user)
|
json_response(self, 201, user)
|
||||||
|
|
||||||
|
def handle_GET_users_username_tokens(self, query):
|
||||||
|
"""GET /api/v1/users/{username}/tokens"""
|
||||||
|
username = require_token(self)
|
||||||
|
if not username:
|
||||||
|
json_response(self, 401, {"message": "invalid authentication"})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Return list of tokens for this user
|
||||||
|
tokens = [t for t in state["tokens"].values() if t.get("username") == username]
|
||||||
|
json_response(self, 200, tokens)
|
||||||
|
|
||||||
def handle_POST_users_username_tokens(self, query):
|
def handle_POST_users_username_tokens(self, query):
|
||||||
"""POST /api/v1/users/{username}/tokens"""
|
"""POST /api/v1/users/{username}/tokens"""
|
||||||
username = require_basic_auth(self)
|
username = require_basic_auth(self)
|
||||||
|
|
@ -305,6 +338,13 @@ class ForgejoHandler(BaseHTTPRequestHandler):
|
||||||
state["tokens"][token_str] = token
|
state["tokens"][token_str] = token
|
||||||
json_response(self, 201, token)
|
json_response(self, 201, token)
|
||||||
|
|
||||||
|
def handle_GET_orgs(self, query):
|
||||||
|
"""GET /api/v1/orgs"""
|
||||||
|
if not require_token(self):
|
||||||
|
json_response(self, 401, {"message": "invalid authentication"})
|
||||||
|
return
|
||||||
|
json_response(self, 200, list(state["orgs"].values()))
|
||||||
|
|
||||||
def handle_POST_orgs(self, query):
|
def handle_POST_orgs(self, query):
|
||||||
"""POST /api/v1/orgs"""
|
"""POST /api/v1/orgs"""
|
||||||
require_token(self)
|
require_token(self)
|
||||||
|
|
@ -374,6 +414,52 @@ class ForgejoHandler(BaseHTTPRequestHandler):
|
||||||
state["repos"][key] = repo
|
state["repos"][key] = repo
|
||||||
json_response(self, 201, repo)
|
json_response(self, 201, repo)
|
||||||
|
|
||||||
|
def handle_POST_users_username_repos(self, query):
|
||||||
|
"""POST /api/v1/users/{username}/repos"""
|
||||||
|
require_token(self)
|
||||||
|
|
||||||
|
parts = self.path.split("/")
|
||||||
|
if len(parts) >= 5:
|
||||||
|
username = parts[4]
|
||||||
|
else:
|
||||||
|
json_response(self, 400, {"message": "username required"})
|
||||||
|
return
|
||||||
|
|
||||||
|
if username 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"{username}/{repo_name}"
|
||||||
|
repo = {
|
||||||
|
"id": repo_id,
|
||||||
|
"full_name": key,
|
||||||
|
"name": repo_name,
|
||||||
|
"owner": {"id": state["users"][username]["id"], "login": username},
|
||||||
|
"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)
|
||||||
|
|
@ -591,6 +677,27 @@ class ForgejoHandler(BaseHTTPRequestHandler):
|
||||||
self.send_header("Content-Length", 0)
|
self.send_header("Content-Length", 0)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
|
def handle_GET_repos_owner_repo_collaborators_collaborator(self, query):
|
||||||
|
"""GET /api/v1/repos/{owner}/{repo}/collaborators/{collaborator}"""
|
||||||
|
require_token(self)
|
||||||
|
|
||||||
|
parts = self.path.split("/")
|
||||||
|
if len(parts) >= 8:
|
||||||
|
owner = parts[4]
|
||||||
|
repo = parts[5]
|
||||||
|
collaborator = parts[7]
|
||||||
|
else:
|
||||||
|
json_response(self, 404, {"message": "repository not found"})
|
||||||
|
return
|
||||||
|
|
||||||
|
key = f"{owner}/{repo}"
|
||||||
|
if key in state["collaborators"] and collaborator in state["collaborators"][key]:
|
||||||
|
self.send_response(204)
|
||||||
|
self.send_header("Content-Length", 0)
|
||||||
|
self.end_headers()
|
||||||
|
else:
|
||||||
|
json_response(self, 404, {"message": "collaborator not found"})
|
||||||
|
|
||||||
def handle_404(self):
|
def handle_404(self):
|
||||||
"""Return 404 for unknown routes."""
|
"""Return 404 for unknown routes."""
|
||||||
json_response(self, 404, {"message": "route not found"})
|
json_response(self, 404, {"message": "route not found"})
|
||||||
|
|
@ -606,13 +713,18 @@ def main():
|
||||||
global SHUTDOWN_REQUESTED
|
global SHUTDOWN_REQUESTED
|
||||||
|
|
||||||
port = int(os.environ.get("MOCK_FORGE_PORT", 3000))
|
port = int(os.environ.get("MOCK_FORGE_PORT", 3000))
|
||||||
server = ThreadingHTTPServer(("0.0.0.0", port), ForgejoHandler)
|
|
||||||
try:
|
try:
|
||||||
server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
server = ThreadingHTTPServer(("0.0.0.0", port), ForgejoHandler)
|
||||||
except OSError:
|
try:
|
||||||
pass # Not all platforms support this
|
server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
except OSError:
|
||||||
|
pass # Not all platforms support this
|
||||||
|
except OSError as e:
|
||||||
|
print(f"Error: Failed to start server on port {port}: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
print(f"Mock Forgejo server starting on port {port}", file=sys.stderr)
|
print(f"Mock Forgejo server starting on port {port}", file=sys.stderr)
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
def shutdown_handler(signum, frame):
|
def shutdown_handler(signum, frame):
|
||||||
global SHUTDOWN_REQUESTED
|
global SHUTDOWN_REQUESTED
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,31 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# tests/smoke-init.sh — End-to-end smoke test for disinto init
|
# tests/smoke-init.sh — End-to-end smoke test for disinto init with mock Forgejo
|
||||||
#
|
#
|
||||||
# Expects a running Forgejo at SMOKE_FORGE_URL with a bootstrap admin
|
# Validates the full init flow using mock Forgejo server:
|
||||||
# user already created (see .woodpecker/smoke-init.yml for CI setup).
|
# 1. Verify mock Forgejo is ready
|
||||||
# Validates the full init flow: Forgejo API, user/token creation,
|
# 2. Set up mock binaries (docker, claude, tmux)
|
||||||
# repo setup, labels, TOML generation, and cron installation.
|
# 3. Run disinto init
|
||||||
|
# 4. Verify Forgejo state (users, repo)
|
||||||
|
# 5. Verify local state (TOML, .env, repo clone)
|
||||||
|
# 6. Verify cron setup
|
||||||
#
|
#
|
||||||
# Required env: SMOKE_FORGE_URL (default: http://localhost:3000)
|
# Required env: FORGE_URL (default: http://localhost:3000)
|
||||||
# Required tools: bash, curl, jq, python3, git
|
# Required tools: bash, curl, jq, python3, git
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
FORGE_URL="${SMOKE_FORGE_URL:-http://localhost:3000}"
|
FORGE_URL="${FORGE_URL:-http://localhost:3000}"
|
||||||
SETUP_ADMIN="setup-admin"
|
|
||||||
SETUP_PASS="SetupPass-789xyz"
|
|
||||||
TEST_SLUG="smoke-org/smoke-repo"
|
|
||||||
MOCK_BIN="/tmp/smoke-mock-bin"
|
MOCK_BIN="/tmp/smoke-mock-bin"
|
||||||
MOCK_STATE="/tmp/smoke-mock-state"
|
TEST_SLUG="smoke-org/smoke-repo"
|
||||||
FAILED=0
|
FAILED=0
|
||||||
|
|
||||||
fail() { printf 'FAIL: %s\n' "$*" >&2; FAILED=1; }
|
fail() { printf 'FAIL: %s\n' "$*" >&2; FAILED=1; }
|
||||||
pass() { printf 'PASS: %s\n' "$*"; }
|
pass() { printf 'PASS: %s\n' "$*"; }
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
rm -rf "$MOCK_BIN" "$MOCK_STATE" /tmp/smoke-test-repo \
|
rm -rf "$MOCK_BIN" /tmp/smoke-test-repo \
|
||||||
"${FACTORY_ROOT}/projects/smoke-repo.toml" \
|
"${FACTORY_ROOT}/projects/smoke-repo.toml"
|
||||||
"${FACTORY_ROOT}/docker-compose.yml"
|
|
||||||
# Restore .env only if we created the backup
|
# Restore .env only if we created the backup
|
||||||
if [ -f "${FACTORY_ROOT}/.env.smoke-backup" ]; then
|
if [ -f "${FACTORY_ROOT}/.env.smoke-backup" ]; then
|
||||||
mv "${FACTORY_ROOT}/.env.smoke-backup" "${FACTORY_ROOT}/.env"
|
mv "${FACTORY_ROOT}/.env.smoke-backup" "${FACTORY_ROOT}/.env"
|
||||||
|
|
@ -40,11 +39,11 @@ trap cleanup EXIT
|
||||||
if [ -f "${FACTORY_ROOT}/.env" ]; then
|
if [ -f "${FACTORY_ROOT}/.env" ]; then
|
||||||
cp "${FACTORY_ROOT}/.env" "${FACTORY_ROOT}/.env.smoke-backup"
|
cp "${FACTORY_ROOT}/.env" "${FACTORY_ROOT}/.env.smoke-backup"
|
||||||
fi
|
fi
|
||||||
# Start with a clean .env (setup_forge writes tokens here)
|
# Start with a clean .env
|
||||||
printf '' > "${FACTORY_ROOT}/.env"
|
printf '' > "${FACTORY_ROOT}/.env"
|
||||||
|
|
||||||
# ── 1. Verify Forgejo is ready ──────────────────────────────────────────────
|
# ── 1. Verify mock Forgejo is ready ─────────────────────────────────────────
|
||||||
echo "=== 1/6 Verifying Forgejo at ${FORGE_URL} ==="
|
echo "=== 1/6 Verifying mock Forgejo at ${FORGE_URL} ==="
|
||||||
retries=0
|
retries=0
|
||||||
api_version=""
|
api_version=""
|
||||||
while true; do
|
while true; do
|
||||||
|
|
@ -55,163 +54,64 @@ while true; do
|
||||||
fi
|
fi
|
||||||
retries=$((retries + 1))
|
retries=$((retries + 1))
|
||||||
if [ "$retries" -gt 30 ]; then
|
if [ "$retries" -gt 30 ]; then
|
||||||
fail "Forgejo API not responding after 30s"
|
fail "Mock Forgejo API not responding after 30s"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
pass "Forgejo API v${api_version} (${retries}s)"
|
pass "Mock Forgejo API v${api_version} (${retries}s)"
|
||||||
|
|
||||||
# Verify bootstrap admin user exists
|
|
||||||
if curl -sf --max-time 5 "${FORGE_URL}/api/v1/users/${SETUP_ADMIN}" >/dev/null 2>&1; then
|
|
||||||
pass "Bootstrap admin '${SETUP_ADMIN}' exists"
|
|
||||||
else
|
|
||||||
fail "Bootstrap admin '${SETUP_ADMIN}' not found — was Forgejo set up?"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 2. Set up mock binaries ─────────────────────────────────────────────────
|
# ── 2. Set up mock binaries ─────────────────────────────────────────────────
|
||||||
echo "=== 2/6 Setting up mock binaries ==="
|
echo "=== 2/6 Setting up mock binaries ==="
|
||||||
mkdir -p "$MOCK_BIN" "$MOCK_STATE"
|
mkdir -p "$MOCK_BIN"
|
||||||
|
|
||||||
# Store bootstrap admin credentials for the docker mock
|
|
||||||
printf '%s:%s' "${SETUP_ADMIN}" "${SETUP_PASS}" > "$MOCK_STATE/bootstrap_creds"
|
|
||||||
|
|
||||||
# ── Mock: docker ──
|
# ── Mock: docker ──
|
||||||
# Routes 'docker exec' user-creation calls to the Forgejo admin API,
|
# Intercepts docker exec calls that disinto init --bare makes to Forgejo CLI
|
||||||
# using the bootstrap admin's credentials.
|
|
||||||
cat > "$MOCK_BIN/docker" << 'DOCKERMOCK'
|
cat > "$MOCK_BIN/docker" << 'DOCKERMOCK'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
FORGE_URL="${SMOKE_FORGE_URL:-${FORGE_URL:-http://localhost:3000}}"
|
||||||
FORGE_URL="${SMOKE_FORGE_URL:-http://localhost:3000}"
|
if [ "${1:-}" = "ps" ]; then exit 0; fi
|
||||||
MOCK_STATE="/tmp/smoke-mock-state"
|
|
||||||
|
|
||||||
if [ ! -f "$MOCK_STATE/bootstrap_creds" ]; then
|
|
||||||
echo "mock-docker: bootstrap credentials not found" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
BOOTSTRAP_CREDS="$(cat "$MOCK_STATE/bootstrap_creds")"
|
|
||||||
|
|
||||||
# docker ps — return empty (no containers running)
|
|
||||||
if [ "${1:-}" = "ps" ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# docker exec — route to Forgejo API
|
|
||||||
if [ "${1:-}" = "exec" ]; then
|
if [ "${1:-}" = "exec" ]; then
|
||||||
shift # remove 'exec'
|
shift
|
||||||
|
|
||||||
# Skip docker exec flags (-u VALUE, -T, -i, etc.)
|
|
||||||
while [ $# -gt 0 ] && [ "${1#-}" != "$1" ]; do
|
while [ $# -gt 0 ] && [ "${1#-}" != "$1" ]; do
|
||||||
case "$1" in
|
case "$1" in -u|-w|-e) shift 2 ;; *) shift ;; esac
|
||||||
-u|-w|-e) shift 2 ;;
|
|
||||||
*) shift ;;
|
|
||||||
esac
|
|
||||||
done
|
done
|
||||||
shift # remove container name (e.g. disinto-forgejo)
|
shift # container name
|
||||||
|
|
||||||
# $@ is now: forgejo admin user list|create [flags]
|
|
||||||
if [ "${1:-}" = "forgejo" ] && [ "${2:-}" = "admin" ] && [ "${3:-}" = "user" ]; then
|
if [ "${1:-}" = "forgejo" ] && [ "${2:-}" = "admin" ] && [ "${3:-}" = "user" ]; then
|
||||||
subcmd="${4:-}"
|
subcmd="${4:-}"
|
||||||
|
if [ "$subcmd" = "list" ]; then echo "ID Username Email"; exit 0; fi
|
||||||
if [ "$subcmd" = "list" ]; then
|
|
||||||
echo "ID Username Email"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$subcmd" = "create" ]; then
|
if [ "$subcmd" = "create" ]; then
|
||||||
shift 4 # skip 'forgejo admin user create'
|
shift 4; username="" password="" email="" is_admin="false"
|
||||||
username="" password="" email="" is_admin="false"
|
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--admin) is_admin="true"; shift ;;
|
--admin) is_admin="true"; shift ;; --username) username="$2"; shift 2 ;;
|
||||||
--username) username="$2"; shift 2 ;;
|
--password) password="$2"; shift 2 ;; --email) email="$2"; shift 2 ;;
|
||||||
--password) password="$2"; shift 2 ;;
|
--must-change-password*) shift ;; *) shift ;;
|
||||||
--email) email="$2"; shift 2 ;;
|
|
||||||
--must-change-password*) shift ;;
|
|
||||||
*) shift ;;
|
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
curl -sf -X POST -H "Content-Type: application/json" \
|
||||||
if [ -z "$username" ] || [ -z "$password" ] || [ -z "$email" ]; then
|
|
||||||
echo "mock-docker: missing required args" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create user via Forgejo admin API
|
|
||||||
if ! curl -sf -X POST \
|
|
||||||
-u "$BOOTSTRAP_CREDS" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${FORGE_URL}/api/v1/admin/users" \
|
"${FORGE_URL}/api/v1/admin/users" \
|
||||||
-d "{\"username\":\"${username}\",\"password\":\"${password}\",\"email\":\"${email}\",\"must_change_password\":false,\"login_name\":\"${username}\",\"source_id\":0}" \
|
-d "{\"username\":\"${username}\",\"password\":\"${password}\",\"email\":\"${email}\",\"must_change_password\":false}" >/dev/null 2>&1
|
||||||
>/dev/null 2>&1; then
|
|
||||||
echo "mock-docker: failed to create user '${username}'" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Patch user: ensure must_change_password is false (Forgejo admin
|
|
||||||
# API POST may ignore it) and promote to admin if requested
|
|
||||||
patch_body="{\"must_change_password\":false,\"login_name\":\"${username}\",\"source_id\":0"
|
|
||||||
if [ "$is_admin" = "true" ]; then
|
if [ "$is_admin" = "true" ]; then
|
||||||
patch_body="${patch_body},\"admin\":true"
|
curl -sf -X PATCH -H "Content-Type: application/json" \
|
||||||
|
"${FORGE_URL}/api/v1/admin/users/${username}" \
|
||||||
|
-d "{\"admin\":true,\"must_change_password\":false}" >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
patch_body="${patch_body}}"
|
echo "New user '${username}' has been successfully created!"; exit 0
|
||||||
|
|
||||||
curl -sf -X PATCH \
|
|
||||||
-u "$BOOTSTRAP_CREDS" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${FORGE_URL}/api/v1/admin/users/${username}" \
|
|
||||||
-d "${patch_body}" \
|
|
||||||
>/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
echo "New user '${username}' has been successfully created!"
|
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$subcmd" = "change-password" ]; then
|
if [ "$subcmd" = "change-password" ]; then
|
||||||
shift 4 # skip 'forgejo admin user change-password'
|
shift 4; username=""
|
||||||
username="" password=""
|
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in --username) username="$2"; shift 2 ;; --password) shift 2 ;; --must-change-password*|--config*) shift ;; *) shift ;; esac
|
||||||
--username) username="$2"; shift 2 ;;
|
|
||||||
--password) password="$2"; shift 2 ;;
|
|
||||||
--must-change-password*) shift ;;
|
|
||||||
--config*) shift ;;
|
|
||||||
*) shift ;;
|
|
||||||
esac
|
|
||||||
done
|
done
|
||||||
|
curl -sf -X PATCH -H "Content-Type: application/json" \
|
||||||
if [ -z "$username" ]; then
|
|
||||||
echo "mock-docker: change-password missing --username" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# PATCH user via Forgejo admin API to clear must_change_password
|
|
||||||
patch_body="{\"must_change_password\":false,\"login_name\":\"${username}\",\"source_id\":0"
|
|
||||||
if [ -n "$password" ]; then
|
|
||||||
patch_body="${patch_body},\"password\":\"${password}\""
|
|
||||||
fi
|
|
||||||
patch_body="${patch_body}}"
|
|
||||||
|
|
||||||
if ! curl -sf -X PATCH \
|
|
||||||
-u "$BOOTSTRAP_CREDS" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${FORGE_URL}/api/v1/admin/users/${username}" \
|
"${FORGE_URL}/api/v1/admin/users/${username}" \
|
||||||
-d "${patch_body}" \
|
-d "{\"must_change_password\":false}" >/dev/null 2>&1 || true
|
||||||
>/dev/null 2>&1; then
|
|
||||||
echo "mock-docker: failed to change-password for '${username}'" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "mock-docker: unhandled exec: $*" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "mock-docker: unhandled command: $*" >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
DOCKERMOCK
|
DOCKERMOCK
|
||||||
chmod +x "$MOCK_BIN/docker"
|
chmod +x "$MOCK_BIN/docker"
|
||||||
|
|
@ -231,11 +131,8 @@ chmod +x "$MOCK_BIN/claude"
|
||||||
printf '#!/usr/bin/env bash\nexit 0\n' > "$MOCK_BIN/tmux"
|
printf '#!/usr/bin/env bash\nexit 0\n' > "$MOCK_BIN/tmux"
|
||||||
chmod +x "$MOCK_BIN/tmux"
|
chmod +x "$MOCK_BIN/tmux"
|
||||||
|
|
||||||
# No crontab mock — use real BusyBox crontab (available in the Forgejo
|
|
||||||
# Alpine image). Cron entries are verified via 'crontab -l' in step 6.
|
|
||||||
|
|
||||||
export PATH="$MOCK_BIN:$PATH"
|
export PATH="$MOCK_BIN:$PATH"
|
||||||
pass "Mock binaries installed (docker, claude, tmux)"
|
pass "Mock binaries installed"
|
||||||
|
|
||||||
# ── 3. Run disinto init ─────────────────────────────────────────────────────
|
# ── 3. Run disinto init ─────────────────────────────────────────────────────
|
||||||
echo "=== 3/6 Running disinto init ==="
|
echo "=== 3/6 Running disinto init ==="
|
||||||
|
|
@ -245,13 +142,26 @@ rm -f "${FACTORY_ROOT}/projects/smoke-repo.toml"
|
||||||
git config --global user.email "smoke@test.local"
|
git config --global user.email "smoke@test.local"
|
||||||
git config --global user.name "Smoke Test"
|
git config --global user.name "Smoke Test"
|
||||||
|
|
||||||
# Alpine containers don't set USER — lib/env.sh needs it
|
# USER needs to be set twice: assignment then export (SC2155)
|
||||||
USER=$(whoami)
|
USER=$(whoami)
|
||||||
export USER
|
export USER
|
||||||
|
|
||||||
|
# Create mock git repo to avoid clone failure (mock server has no git support)
|
||||||
|
mkdir -p "/tmp/smoke-test-repo"
|
||||||
|
cd "/tmp/smoke-test-repo"
|
||||||
|
git init --quiet
|
||||||
|
git config user.email "smoke@test.local"
|
||||||
|
git config user.name "Smoke Test"
|
||||||
|
echo "# smoke-repo" > README.md
|
||||||
|
git add README.md
|
||||||
|
git commit --quiet -m "Initial commit"
|
||||||
|
|
||||||
export SMOKE_FORGE_URL="$FORGE_URL"
|
export SMOKE_FORGE_URL="$FORGE_URL"
|
||||||
export FORGE_URL
|
export FORGE_URL
|
||||||
|
|
||||||
|
# Skip push to mock server (no git support)
|
||||||
|
export SKIP_PUSH=true
|
||||||
|
|
||||||
if bash "${FACTORY_ROOT}/bin/disinto" init \
|
if bash "${FACTORY_ROOT}/bin/disinto" init \
|
||||||
"${TEST_SLUG}" \
|
"${TEST_SLUG}" \
|
||||||
--bare --yes \
|
--bare --yes \
|
||||||
|
|
@ -294,35 +204,6 @@ if [ "$repo_found" = false ]; then
|
||||||
fail "Repo not found on Forgejo under any expected path"
|
fail "Repo not found on Forgejo under any expected path"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Labels exist on repo — use bootstrap admin to check
|
|
||||||
setup_token=$(curl -sf -X POST \
|
|
||||||
-u "${SETUP_ADMIN}:${SETUP_PASS}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${FORGE_URL}/api/v1/users/${SETUP_ADMIN}/tokens" \
|
|
||||||
-d '{"name":"smoke-verify","scopes":["all"]}' 2>/dev/null \
|
|
||||||
| jq -r '.sha1 // empty') || setup_token=""
|
|
||||||
|
|
||||||
if [ -n "$setup_token" ]; then
|
|
||||||
label_count=0
|
|
||||||
for repo_path in "${TEST_SLUG}" "dev-bot/smoke-repo" "disinto-admin/smoke-repo"; do
|
|
||||||
label_count=$(curl -sf \
|
|
||||||
-H "Authorization: token ${setup_token}" \
|
|
||||||
"${FORGE_URL}/api/v1/repos/${repo_path}/labels?limit=50" 2>/dev/null \
|
|
||||||
| jq 'length' 2>/dev/null) || label_count=0
|
|
||||||
if [ "$label_count" -gt 0 ]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$label_count" -ge 5 ]; then
|
|
||||||
pass "Labels created on repo (${label_count} labels)"
|
|
||||||
else
|
|
||||||
fail "Expected >= 5 labels, found ${label_count}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
fail "Could not obtain verification token from bootstrap admin"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 5. Verify local state ───────────────────────────────────────────────────
|
# ── 5. Verify local state ───────────────────────────────────────────────────
|
||||||
echo "=== 5/6 Verifying local state ==="
|
echo "=== 5/6 Verifying local state ==="
|
||||||
|
|
||||||
|
|
@ -361,7 +242,7 @@ else
|
||||||
fail ".env not found"
|
fail ".env not found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Repo was cloned
|
# Repo was cloned (mock git repo created before disinto init)
|
||||||
if [ -d "/tmp/smoke-test-repo/.git" ]; then
|
if [ -d "/tmp/smoke-test-repo/.git" ]; then
|
||||||
pass "Repo cloned to /tmp/smoke-test-repo"
|
pass "Repo cloned to /tmp/smoke-test-repo"
|
||||||
else
|
else
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue