From 9c2a5634ffdef0f70b326e1de221a52f8f5ee8fc Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 25 Mar 2026 09:37:36 +0000 Subject: [PATCH 1/6] fix: feat: end-to-end disinto init smoke test in CI (#668) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .woodpecker/smoke-init.yml | 24 +++ tests/smoke-init.sh | 430 +++++++++++++++++++++++++++++++++++++ 2 files changed, 454 insertions(+) create mode 100644 .woodpecker/smoke-init.yml create mode 100644 tests/smoke-init.sh diff --git a/.woodpecker/smoke-init.yml b/.woodpecker/smoke-init.yml new file mode 100644 index 0000000..d863f97 --- /dev/null +++ b/.woodpecker/smoke-init.yml @@ -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 diff --git a/tests/smoke-init.sh b/tests/smoke-init.sh new file mode 100644 index 0000000..61338de --- /dev/null +++ b/tests/smoke-init.sh @@ -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 ===" From 78e478e69d4c902b29214b6d8074457013af1b25 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 25 Mar 2026 09:58:47 +0000 Subject: [PATCH 2/6] fix: use Forgejo image as step container for CLI access (#668) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install endpoint POST returned 404 because FORGEJO__database__DB_TYPE env var auto-configured Forgejo, bypassing install mode. Fix: run the Forgejo image as the step container instead of a service. This gives CLI access to `forgejo admin user create` for bootstrap admin setup — no install endpoint needed. Co-Authored-By: Claude Opus 4.6 (1M context) --- .woodpecker/smoke-init.yml | 29 +++++---- tests/smoke-init.sh | 121 +++++++++---------------------------- 2 files changed, 43 insertions(+), 107 deletions(-) diff --git a/.woodpecker/smoke-init.yml b/.woodpecker/smoke-init.yml index d863f97..0e9ace7 100644 --- a/.woodpecker/smoke-init.yml +++ b/.woodpecker/smoke-init.yml @@ -1,24 +1,27 @@ # .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. +# 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. 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 + image: codeberg.org/forgejo/forgejo:11.0 environment: - SMOKE_FORGE_URL: http://forgejo:3000 + SMOKE_FORGE_URL: http://localhost: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 + # 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 + - 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 + # Start Forgejo in background and wait for it + - 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 (this is why we use the Forgejo image) + - 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 - bash tests/smoke-init.sh diff --git a/tests/smoke-init.sh b/tests/smoke-init.sh index 61338de..a529956 100644 --- a/tests/smoke-init.sh +++ b/tests/smoke-init.sh @@ -1,20 +1,20 @@ #!/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). +# 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://forgejo:3000) +# 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://forgejo:3000}" +FORGE_URL="${SMOKE_FORGE_URL:-http://localhost: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" @@ -24,8 +24,8 @@ 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" \ + 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 @@ -43,68 +43,8 @@ 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" +# ── 1. Verify Forgejo is ready ────────────────────────────────────────────── +echo "=== 1/6 Verifying Forgejo at ${FORGE_URL} ===" retries=0 api_version="" while true; do @@ -114,26 +54,24 @@ while true; do break fi retries=$((retries + 1)) - if [ "$retries" -gt 60 ]; then - echo "" - fail "Forgejo API not functional after install (60s)" + if [ "$retries" -gt 30 ]; then + fail "Forgejo API not responding after 30s" exit 1 fi - echo -n "." sleep 1 done -echo " ready (v${api_version})" +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}' created via install endpoint" + pass "Bootstrap admin '${SETUP_ADMIN}' exists" else - fail "Bootstrap admin '${SETUP_ADMIN}' not found — install may have failed" + fail "Bootstrap admin '${SETUP_ADMIN}' not found — was Forgejo set up?" exit 1 fi # ── 2. Set up mock binaries ───────────────────────────────────────────────── -echo "=== 2/7 Setting up mock binaries ===" +echo "=== 2/6 Setting up mock binaries ===" mkdir -p "$MOCK_BIN" "$MOCK_STATE" # Store bootstrap admin credentials for the docker mock @@ -146,7 +84,7 @@ cat > "$MOCK_BIN/docker" << 'DOCKERMOCK' #!/usr/bin/env bash set -euo pipefail -FORGE_URL="${SMOKE_FORGE_URL:-http://forgejo:3000}" +FORGE_URL="${SMOKE_FORGE_URL:-http://localhost:3000}" MOCK_STATE="/tmp/smoke-mock-state" if [ ! -f "$MOCK_STATE/bootstrap_creds" ]; then @@ -266,20 +204,15 @@ 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) +# ── 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" -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 \ @@ -292,8 +225,8 @@ else fail "disinto init exited non-zero" fi -# ── 5. Verify Forgejo state ───────────────────────────────────────────────── -echo "=== 5/7 Verifying Forgejo state ===" +# ── 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 @@ -311,7 +244,7 @@ for bot in dev-bot review-bot; do fi done -# Repo exists (try org path, then fallback to dev-bot user path) +# 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 @@ -353,8 +286,8 @@ else fail "Could not obtain verification token from bootstrap admin" fi -# ── 6. Verify local state ─────────────────────────────────────────────────── -echo "=== 6/7 Verifying local state ===" +# ── 5. Verify local state ─────────────────────────────────────────────────── +echo "=== 5/6 Verifying local state ===" # TOML was generated toml_path="${FACTORY_ROOT}/projects/smoke-repo.toml" @@ -398,8 +331,8 @@ else fail "Repo not cloned to /tmp/smoke-test-repo" fi -# ── 7. Verify cron setup ──────────────────────────────────────────────────── -echo "=== 7/7 Verifying cron setup ===" +# ── 6. Verify cron setup ──────────────────────────────────────────────────── +echo "=== 6/6 Verifying cron setup ===" cron_file="$MOCK_STATE/crontab-entries" if [ -f "$cron_file" ]; then if grep -q 'dev-poll.sh' "$cron_file"; then From 55a22912d3edcbf39740cc33c8781bf02ac1dea5 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 25 Mar 2026 10:19:31 +0000 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20run=20Forgejo=20as=20git=20user=20?= =?UTF-8?q?=E2=80=94=20refuses=20to=20run=20as=20root=20(#668)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forgejo 11.0 refuses to run as root with a fatal error. Use su-exec to run all forgejo commands as the 'git' user (pre-created in the Forgejo Docker image). chown /data to git:git before starting. Co-Authored-By: Claude Opus 4.6 (1M context) --- .woodpecker/smoke-init.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.woodpecker/smoke-init.yml b/.woodpecker/smoke-init.yml index 0e9ace7..e156c15 100644 --- a/.woodpecker/smoke-init.yml +++ b/.woodpecker/smoke-init.yml @@ -3,6 +3,9 @@ # 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] @@ -15,13 +18,14 @@ steps: 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 + # 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 - # Start Forgejo in background and wait for it - - forgejo web --config /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 (this is why we use the Forgejo image) - - 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 + # 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 From c643cf16dc60eeed7a9f404fa0d293d3d2096171 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 25 Mar 2026 11:06:01 +0000 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20use=20basic=20auth=20for=20bot=20tok?= =?UTF-8?q?en=20creation=20=E2=80=94=20Forgejo=20rejects=20token=20auth=20?= =?UTF-8?q?(#668)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /api/v1/users/{username}/tokens requires basic auth (reqBasicOrRevProxyAuth) in Forgejo 11.x. The previous code used admin token auth which returns 401. Fix: authenticate as the bot user with -u "${bot_user}:${bot_pass}" instead of -H "Authorization: token ${admin_token}". The bot_pass is available in scope from the user creation step. Bug caught by the new smoke-init end-to-end test. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/disinto | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/disinto b/bin/disinto index c8e420a..1f7f1d3 100755 --- a/bin/disinto +++ b/bin/disinto @@ -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 \ From 39aa638b6f9417827ac8891de10288213fc8fa7e Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 25 Mar 2026 11:13:31 +0000 Subject: [PATCH 5/6] fix: PATCH all mock users to disable must_change_password (#668) Forgejo's admin API POST /admin/users may not honor must_change_password:false in the request body. Previously only admin users got a PATCH (to set admin:true), which incidentally cleared must_change_password. Bot users had no PATCH, so basic auth for token creation returned 401. Now every mock-created user gets a PATCH to explicitly set must_change_password:false, fixing bot token creation. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/smoke-init.sh | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/smoke-init.sh b/tests/smoke-init.sh index a529956..eccd098 100644 --- a/tests/smoke-init.sh +++ b/tests/smoke-init.sh @@ -150,15 +150,20 @@ if [ "${1:-}" = "exec" ]; then exit 1 fi - # Promote to site admin if requested + # 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 - 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 + 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 From 14b2abd9cda13f5684f82a0f9566285ff3054180 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 25 Mar 2026 11:23:41 +0000 Subject: [PATCH 6/6] fix: use real BusyBox crontab instead of mock for cron verification (#668) The mock crontab file was not being created despite PATH precedence working correctly. Replace the mock with the real BusyBox crontab already available in the Forgejo Alpine image. Verify cron entries via 'crontab -l' output instead of checking a mock state file. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/smoke-init.sh | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/tests/smoke-init.sh b/tests/smoke-init.sh index eccd098..d9c4f35 100644 --- a/tests/smoke-init.sh +++ b/tests/smoke-init.sh @@ -194,20 +194,11 @@ chmod +x "$MOCK_BIN/claude" 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" +# 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, crontab)" +pass "Mock binaries installed (docker, claude, tmux)" # ── 3. Run disinto init ───────────────────────────────────────────────────── echo "=== 3/6 Running disinto init ===" @@ -338,25 +329,25 @@ fi # ── 6. Verify cron setup ──────────────────────────────────────────────────── echo "=== 6/6 Verifying cron setup ===" -cron_file="$MOCK_STATE/crontab-entries" -if [ -f "$cron_file" ]; then - if grep -q 'dev-poll.sh' "$cron_file"; then +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 grep -q 'review-poll.sh' "$cron_file"; then + 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 grep -q 'gardener-run.sh' "$cron_file"; then + 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 captured (mock crontab file missing)" + fail "No cron entries found (crontab -l returned empty)" fi # ── Summary ──────────────────────────────────────────────────────────────────