Merge pull request 'fix: feat: end-to-end disinto init smoke test in CI (#668)' (#675) from fix/issue-668 into main
This commit is contained in:
commit
d13f1a6997
3 changed files with 394 additions and 3 deletions
31
.woodpecker/smoke-init.yml
Normal file
31
.woodpecker/smoke-init.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# .woodpecker/smoke-init.yml — End-to-end smoke test for disinto init
|
||||
#
|
||||
# Uses the Forgejo image directly (not as a service) so we have CLI
|
||||
# access to set up Forgejo and create the bootstrap admin user.
|
||||
# Then runs disinto init --bare --yes against the local Forgejo instance.
|
||||
#
|
||||
# Forgejo refuses to run as root, so all forgejo commands use su-exec
|
||||
# to run as the 'git' user (pre-created in the Forgejo Docker image).
|
||||
|
||||
when:
|
||||
event: [push, pull_request]
|
||||
|
||||
steps:
|
||||
- name: smoke-init
|
||||
image: codeberg.org/forgejo/forgejo:11.0
|
||||
environment:
|
||||
SMOKE_FORGE_URL: http://localhost:3000
|
||||
commands:
|
||||
# Install test dependencies (Alpine-based image)
|
||||
- apk add --no-cache bash curl jq python3 git >/dev/null 2>&1
|
||||
# Set up Forgejo data directories and config (owned by git user)
|
||||
- mkdir -p /data/gitea/conf /data/gitea/repositories /data/gitea/lfs /data/gitea/log /data/git/.ssh /data/ssh
|
||||
- printf '[database]\nDB_TYPE = sqlite3\nPATH = /data/gitea/forgejo.db\n\n[server]\nHTTP_PORT = 3000\nROOT_URL = http://localhost:3000/\nLFS_START_SERVER = false\n\n[security]\nINSTALL_LOCK = true\n\n[service]\nDISABLE_REGISTRATION = true\n' > /data/gitea/conf/app.ini
|
||||
- chown -R git:git /data
|
||||
# Start Forgejo as git user in background and wait for API
|
||||
- su-exec git forgejo web --config /data/gitea/conf/app.ini &
|
||||
- for i in $(seq 1 30); do curl -sf http://localhost:3000/api/v1/version >/dev/null 2>&1 && break; sleep 1; done
|
||||
# Create bootstrap admin user via CLI
|
||||
- su-exec git forgejo admin user create --admin --username setup-admin --password "SetupPass-789xyz" --email "setup-admin@smoke.test" --must-change-password=false --config /data/gitea/conf/app.ini
|
||||
# Run the smoke test (as root is fine — only forgejo binary needs git user)
|
||||
- bash tests/smoke-init.sh
|
||||
|
|
@ -456,10 +456,11 @@ setup_forge() {
|
|||
fi
|
||||
fi
|
||||
|
||||
# Generate token via API (using admin credentials for the bot)
|
||||
# Generate token via API (basic auth as the bot user — Forgejo requires
|
||||
# basic auth on POST /users/{username}/tokens, token auth is rejected)
|
||||
local token
|
||||
token=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${admin_token}" \
|
||||
-u "${bot_user}:${bot_pass}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${forge_url}/api/v1/users/${bot_user}/tokens" \
|
||||
-d "{\"name\":\"disinto-${bot_user}-token\",\"scopes\":[\"all\"]}" 2>/dev/null \
|
||||
|
|
@ -468,7 +469,7 @@ setup_forge() {
|
|||
if [ -z "$token" ]; then
|
||||
# Token name collision — create with timestamp suffix
|
||||
token=$(curl -sf -X POST \
|
||||
-H "Authorization: token ${admin_token}" \
|
||||
-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 \
|
||||
|
|
|
|||
359
tests/smoke-init.sh
Normal file
359
tests/smoke-init.sh
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
#!/usr/bin/env bash
|
||||
# tests/smoke-init.sh — End-to-end smoke test for disinto init
|
||||
#
|
||||
# Expects a running Forgejo at SMOKE_FORGE_URL with a bootstrap admin
|
||||
# user already created (see .woodpecker/smoke-init.yml for CI setup).
|
||||
# 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://localhost:3000)
|
||||
# Required tools: bash, curl, jq, python3, git
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
FORGE_URL="${SMOKE_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_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 \
|
||||
"${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"
|
||||
|
||||
# ── 1. Verify Forgejo is ready ──────────────────────────────────────────────
|
||||
echo "=== 1/6 Verifying Forgejo at ${FORGE_URL} ==="
|
||||
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 30 ]; then
|
||||
fail "Forgejo API not responding after 30s"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
pass "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 ─────────────────────────────────────────────────
|
||||
echo "=== 2/6 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://localhost: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
|
||||
|
||||
# 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
|
||||
patch_body="${patch_body},\"admin\":true"
|
||||
fi
|
||||
patch_body="${patch_body}}"
|
||||
|
||||
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
|
||||
|
||||
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"
|
||||
|
||||
# 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"
|
||||
pass "Mock binaries installed (docker, claude, tmux)"
|
||||
|
||||
# ── 3. Run disinto init ─────────────────────────────────────────────────────
|
||||
echo "=== 3/6 Running disinto init ==="
|
||||
rm -f "${FACTORY_ROOT}/projects/smoke-repo.toml"
|
||||
|
||||
# Configure git identity (needed for git operations)
|
||||
git config --global user.email "smoke@test.local"
|
||||
git config --global user.name "Smoke Test"
|
||||
|
||||
export SMOKE_FORGE_URL="$FORGE_URL"
|
||||
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
|
||||
|
||||
# ── 4. Verify Forgejo state ─────────────────────────────────────────────────
|
||||
echo "=== 4/6 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 paths)
|
||||
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
|
||||
|
||||
# ── 5. Verify local state ───────────────────────────────────────────────────
|
||||
echo "=== 5/6 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
|
||||
|
||||
# ── 6. Verify cron setup ────────────────────────────────────────────────────
|
||||
echo "=== 6/6 Verifying cron setup ==="
|
||||
cron_output=$(crontab -l 2>/dev/null) || cron_output=""
|
||||
if [ -n "$cron_output" ]; then
|
||||
if printf '%s' "$cron_output" | grep -q 'dev-poll.sh'; then
|
||||
pass "Cron includes dev-poll entry"
|
||||
else
|
||||
fail "Cron missing dev-poll entry"
|
||||
fi
|
||||
if printf '%s' "$cron_output" | grep -q 'review-poll.sh'; then
|
||||
pass "Cron includes review-poll entry"
|
||||
else
|
||||
fail "Cron missing review-poll entry"
|
||||
fi
|
||||
if printf '%s' "$cron_output" | grep -q 'gardener-run.sh'; then
|
||||
pass "Cron includes gardener entry"
|
||||
else
|
||||
fail "Cron missing gardener entry"
|
||||
fi
|
||||
else
|
||||
fail "No cron entries found (crontab -l returned empty)"
|
||||
fi
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
if [ "$FAILED" -ne 0 ]; then
|
||||
echo "=== SMOKE-INIT TEST FAILED ==="
|
||||
exit 1
|
||||
fi
|
||||
echo "=== SMOKE-INIT TEST PASSED ==="
|
||||
Loading…
Add table
Add a link
Reference in a new issue