fix: feat: end-to-end disinto init smoke test in CI (#668)

Add tests/smoke-init.sh — an end-to-end smoke test that runs
disinto init --bare --yes against a real Forgejo instance
(started as a Woodpecker service container).

The test validates:
- Forgejo API responds after init
- Admin and bot users created with tokens
- Repo created with labels on Forgejo
- Project TOML generated correctly
- .env written with FORGE_TOKEN and FORGE_REVIEW_TOKEN
- Cron entries installed (dev-poll, review-poll, gardener)

Uses mock binaries for docker (routes user creation to Forgejo
admin API), claude, tmux, and crontab to run in CI without
Docker-in-Docker.

Wired into CI via .woodpecker/smoke-init.yml (separate pipeline
with Forgejo service, runs on push and pull_request).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-25 09:37:36 +00:00
parent b2dd42df40
commit 9c2a5634ff
2 changed files with 454 additions and 0 deletions

View file

@ -0,0 +1,24 @@
# .woodpecker/smoke-init.yml — End-to-end smoke test for disinto init
#
# Starts a real Forgejo instance as a service container, then runs
# disinto init --bare --yes against it and verifies the results.
when:
event: [push, pull_request]
services:
- name: forgejo
image: codeberg.org/forgejo/forgejo:11.0
environment:
FORGEJO__database__DB_TYPE: sqlite3
FORGEJO__server__ROOT_URL: "http://forgejo:3000/"
FORGEJO__server__HTTP_PORT: "3000"
steps:
- name: smoke-init
image: debian:bookworm-slim
environment:
SMOKE_FORGE_URL: http://forgejo:3000
commands:
- apt-get update -qq && apt-get install -y -qq --no-install-recommends bash curl jq python3 git ca-certificates >/dev/null 2>&1
- bash tests/smoke-init.sh

430
tests/smoke-init.sh Normal file
View file

@ -0,0 +1,430 @@
#!/usr/bin/env bash
# tests/smoke-init.sh — End-to-end smoke test for disinto init
#
# Runs against a real Forgejo instance (Woodpecker service container).
# Validates the full init flow: Forgejo API, user/token creation,
# repo setup, labels, TOML generation, and cron installation.
#
# Required env: SMOKE_FORGE_URL (default: http://forgejo:3000)
# Required tools: bash, curl, jq, python3, git
set -euo pipefail
FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
FORGE_URL="${SMOKE_FORGE_URL:-http://forgejo:3000}"
SETUP_ADMIN="setup-admin"
SETUP_PASS="SetupPass-789xyz"
SETUP_EMAIL="setup-admin@smoke.test"
TEST_SLUG="smoke-org/smoke-repo"
MOCK_BIN="/tmp/smoke-mock-bin"
MOCK_STATE="/tmp/smoke-mock-state"
FAILED=0
fail() { printf 'FAIL: %s\n' "$*" >&2; FAILED=1; }
pass() { printf 'PASS: %s\n' "$*"; }
cleanup() {
rm -rf "$MOCK_BIN" "$MOCK_STATE" /tmp/smoke-test-repo /tmp/forgejo-cookies \
/tmp/install-page.html "${FACTORY_ROOT}/projects/smoke-repo.toml" \
"${FACTORY_ROOT}/docker-compose.yml"
# Restore .env only if we created the backup
if [ -f "${FACTORY_ROOT}/.env.smoke-backup" ]; then
mv "${FACTORY_ROOT}/.env.smoke-backup" "${FACTORY_ROOT}/.env"
else
rm -f "${FACTORY_ROOT}/.env"
fi
}
trap cleanup EXIT
# Back up existing .env if present
if [ -f "${FACTORY_ROOT}/.env" ]; then
cp "${FACTORY_ROOT}/.env" "${FACTORY_ROOT}/.env.smoke-backup"
fi
# Start with a clean .env (setup_forge writes tokens here)
printf '' > "${FACTORY_ROOT}/.env"
# ── 0. Wait for Forgejo HTTP ────────────────────────────────────────────────
echo "=== 0/7 Waiting for Forgejo at ${FORGE_URL} ==="
retries=0
while true; do
if curl -sf --max-time 3 -o /dev/null "${FORGE_URL}/" 2>/dev/null; then
break
fi
retries=$((retries + 1))
if [ "$retries" -gt 90 ]; then
fail "Forgejo not responsive after 90s"
exit 1
fi
sleep 1
done
pass "Forgejo HTTP responsive (${retries}s)"
# ── 1. Complete Forgejo install ──────────────────────────────────────────────
echo "=== 1/7 Completing Forgejo initial setup ==="
# GET the install page to obtain CSRF token and session cookie
curl -sf -c /tmp/forgejo-cookies "${FORGE_URL}/install" \
-o /tmp/install-page.html 2>/dev/null || true
# Extract CSRF token (hidden input or meta tag)
csrf_token=""
if [ -f /tmp/install-page.html ]; then
csrf_token=$(grep '_csrf' /tmp/install-page.html \
| grep -oE '(content|value)="[^"]*"' \
| head -1 | cut -d'"' -f2) || csrf_token=""
fi
# Build POST data array
post_args=(
--data-urlencode "db_type=SQLite3"
--data-urlencode "db_path=/data/gitea/gitea.db"
--data-urlencode "app_name=Forgejo"
--data-urlencode "repo_root_path=/data/gitea/repositories"
--data-urlencode "lfs_root_path=/data/gitea/lfs"
--data-urlencode "run_user=git"
--data-urlencode "domain=forgejo"
--data-urlencode "ssh_port=22"
--data-urlencode "http_port=3000"
--data-urlencode "app_url=${FORGE_URL}/"
--data-urlencode "log_root_path=/data/gitea/log"
--data-urlencode "admin_name=${SETUP_ADMIN}"
--data-urlencode "admin_passwd=${SETUP_PASS}"
--data-urlencode "admin_confirm_passwd=${SETUP_PASS}"
--data-urlencode "admin_email=${SETUP_EMAIL}"
)
if [ -n "$csrf_token" ]; then
post_args+=(--data-urlencode "_csrf=${csrf_token}")
fi
install_code=$(curl -s -b /tmp/forgejo-cookies \
-o /dev/null -w '%{http_code}' \
-X POST "${FORGE_URL}/install" \
"${post_args[@]}" 2>/dev/null) || install_code="000"
echo "Install POST returned HTTP ${install_code}"
# Wait for Forgejo API to become functional after install
echo -n "Waiting for Forgejo API"
retries=0
api_version=""
while true; do
api_version=$(curl -sf --max-time 3 "${FORGE_URL}/api/v1/version" 2>/dev/null \
| jq -r '.version // empty' 2>/dev/null) || api_version=""
if [ -n "$api_version" ]; then
break
fi
retries=$((retries + 1))
if [ "$retries" -gt 60 ]; then
echo ""
fail "Forgejo API not functional after install (60s)"
exit 1
fi
echo -n "."
sleep 1
done
echo " ready (v${api_version})"
# 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}' created via install endpoint"
else
fail "Bootstrap admin '${SETUP_ADMIN}' not found — install may have failed"
exit 1
fi
# ── 2. Set up mock binaries ─────────────────────────────────────────────────
echo "=== 2/7 Setting up mock binaries ==="
mkdir -p "$MOCK_BIN" "$MOCK_STATE"
# Store bootstrap admin credentials for the docker mock
printf '%s:%s' "${SETUP_ADMIN}" "${SETUP_PASS}" > "$MOCK_STATE/bootstrap_creds"
# ── Mock: docker ──
# Routes 'docker exec' user-creation calls to the Forgejo admin API,
# using the bootstrap admin's credentials.
cat > "$MOCK_BIN/docker" << 'DOCKERMOCK'
#!/usr/bin/env bash
set -euo pipefail
FORGE_URL="${SMOKE_FORGE_URL:-http://forgejo:3000}"
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
shift # remove 'exec'
# Skip docker exec flags (-u VALUE, -T, -i, etc.)
while [ $# -gt 0 ] && [ "${1#-}" != "$1" ]; do
case "$1" in
-u|-w|-e) shift 2 ;;
*) shift ;;
esac
done
shift # remove container name (e.g. disinto-forgejo)
# $@ is now: forgejo admin user list|create [flags]
if [ "${1:-}" = "forgejo" ] && [ "${2:-}" = "admin" ] && [ "${3:-}" = "user" ]; then
subcmd="${4:-}"
if [ "$subcmd" = "list" ]; then
echo "ID Username Email"
exit 0
fi
if [ "$subcmd" = "create" ]; then
shift 4 # skip 'forgejo admin user create'
username="" password="" email="" is_admin="false"
while [ $# -gt 0 ]; do
case "$1" in
--admin) is_admin="true"; shift ;;
--username) username="$2"; shift 2 ;;
--password) password="$2"; shift 2 ;;
--email) email="$2"; shift 2 ;;
--must-change-password*) shift ;;
*) shift ;;
esac
done
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" \
-d "{\"username\":\"${username}\",\"password\":\"${password}\",\"email\":\"${email}\",\"must_change_password\":false,\"login_name\":\"${username}\",\"source_id\":0}" \
>/dev/null 2>&1; then
echo "mock-docker: failed to create user '${username}'" >&2
exit 1
fi
# Promote to site admin if requested
if [ "$is_admin" = "true" ]; then
curl -sf -X PATCH \
-u "$BOOTSTRAP_CREDS" \
-H "Content-Type: application/json" \
"${FORGE_URL}/api/v1/admin/users/${username}" \
-d "{\"admin\":true,\"login_name\":\"${username}\",\"source_id\":0}" \
>/dev/null 2>&1 || true
fi
echo "New user '${username}' has been successfully created!"
exit 0
fi
fi
echo "mock-docker: unhandled exec: $*" >&2
exit 1
fi
echo "mock-docker: unhandled command: $*" >&2
exit 1
DOCKERMOCK
chmod +x "$MOCK_BIN/docker"
# ── Mock: claude ──
cat > "$MOCK_BIN/claude" << 'CLAUDEMOCK'
#!/usr/bin/env bash
case "$*" in
*"auth status"*) printf '{"loggedIn":true}\n' ;;
*"--version"*) printf 'claude 1.0.0 (mock)\n' ;;
esac
exit 0
CLAUDEMOCK
chmod +x "$MOCK_BIN/claude"
# ── Mock: tmux ──
printf '#!/usr/bin/env bash\nexit 0\n' > "$MOCK_BIN/tmux"
chmod +x "$MOCK_BIN/tmux"
# ── Mock: crontab ──
cat > "$MOCK_BIN/crontab" << 'CRONMOCK'
#!/usr/bin/env bash
CRON_FILE="/tmp/smoke-mock-state/crontab-entries"
case "${1:-}" in
-l) cat "$CRON_FILE" 2>/dev/null || true ;;
-) cat > "$CRON_FILE" ;;
*) exit 0 ;;
esac
CRONMOCK
chmod +x "$MOCK_BIN/crontab"
export PATH="$MOCK_BIN:$PATH"
pass "Mock binaries installed (docker, claude, tmux, crontab)"
# ── 3. Prepare source repo ──────────────────────────────────────────────────
echo "=== 3/7 Preparing source repo ==="
# Configure git identity for the test (needed for any git operations)
git config --global user.email "smoke@test.local"
git config --global user.name "Smoke Test"
pass "Git configured"
# ── 4. Run disinto init ─────────────────────────────────────────────────────
echo "=== 4/7 Running disinto init ==="
rm -f "${FACTORY_ROOT}/projects/smoke-repo.toml"
export SMOKE_FORGE_URL="$FORGE_URL"
# FORGE_URL is read by lib/env.sh; override so init uses our test Forgejo
export FORGE_URL
if bash "${FACTORY_ROOT}/bin/disinto" init \
"${TEST_SLUG}" \
--bare --yes \
--forge-url "$FORGE_URL" \
--repo-root "/tmp/smoke-test-repo"; then
pass "disinto init completed successfully"
else
fail "disinto init exited non-zero"
fi
# ── 5. Verify Forgejo state ─────────────────────────────────────────────────
echo "=== 5/7 Verifying Forgejo state ==="
# Admin user exists
if curl -sf --max-time 5 "${FORGE_URL}/api/v1/users/disinto-admin" >/dev/null 2>&1; then
pass "Admin user 'disinto-admin' exists on Forgejo"
else
fail "Admin user 'disinto-admin' not found on Forgejo"
fi
# Bot users exist
for bot in dev-bot review-bot; do
if curl -sf --max-time 5 "${FORGE_URL}/api/v1/users/${bot}" >/dev/null 2>&1; then
pass "Bot user '${bot}' exists on Forgejo"
else
fail "Bot user '${bot}' not found on Forgejo"
fi
done
# Repo exists (try org path, then fallback to dev-bot user path)
repo_found=false
for repo_path in "${TEST_SLUG}" "dev-bot/smoke-repo" "disinto-admin/smoke-repo"; do
if curl -sf --max-time 5 "${FORGE_URL}/api/v1/repos/${repo_path}" >/dev/null 2>&1; then
pass "Repo '${repo_path}' exists on Forgejo"
repo_found=true
break
fi
done
if [ "$repo_found" = false ]; then
fail "Repo not found on Forgejo under any expected path"
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
# ── 6. Verify local state ───────────────────────────────────────────────────
echo "=== 6/7 Verifying local state ==="
# TOML was generated
toml_path="${FACTORY_ROOT}/projects/smoke-repo.toml"
if [ -f "$toml_path" ]; then
toml_name=$(python3 -c "
import tomllib, sys
with open(sys.argv[1], 'rb') as f:
print(tomllib.load(f)['name'])
" "$toml_path" 2>/dev/null) || toml_name=""
if [ "$toml_name" = "smoke-repo" ]; then
pass "TOML generated with correct project name"
else
fail "TOML name mismatch: expected 'smoke-repo', got '${toml_name}'"
fi
else
fail "TOML not generated at ${toml_path}"
fi
# .env has tokens
env_file="${FACTORY_ROOT}/.env"
if [ -f "$env_file" ]; then
if grep -q '^FORGE_TOKEN=' "$env_file"; then
pass ".env contains FORGE_TOKEN"
else
fail ".env missing FORGE_TOKEN"
fi
if grep -q '^FORGE_REVIEW_TOKEN=' "$env_file"; then
pass ".env contains FORGE_REVIEW_TOKEN"
else
fail ".env missing FORGE_REVIEW_TOKEN"
fi
else
fail ".env not found"
fi
# Repo was cloned
if [ -d "/tmp/smoke-test-repo/.git" ]; then
pass "Repo cloned to /tmp/smoke-test-repo"
else
fail "Repo not cloned to /tmp/smoke-test-repo"
fi
# ── 7. Verify cron setup ────────────────────────────────────────────────────
echo "=== 7/7 Verifying cron setup ==="
cron_file="$MOCK_STATE/crontab-entries"
if [ -f "$cron_file" ]; then
if grep -q 'dev-poll.sh' "$cron_file"; then
pass "Cron includes dev-poll entry"
else
fail "Cron missing dev-poll entry"
fi
if grep -q 'review-poll.sh' "$cron_file"; then
pass "Cron includes review-poll entry"
else
fail "Cron missing review-poll entry"
fi
if grep -q 'gardener-run.sh' "$cron_file"; then
pass "Cron includes gardener entry"
else
fail "Cron missing gardener entry"
fi
else
fail "No cron entries captured (mock crontab file missing)"
fi
# ── Summary ──────────────────────────────────────────────────────────────────
echo ""
if [ "$FAILED" -ne 0 ]; then
echo "=== SMOKE-INIT TEST FAILED ==="
exit 1
fi
echo "=== SMOKE-INIT TEST PASSED ==="