Compare commits

...
11 Commits
Author SHA1 Message Date
Outis b3b167ba8d Merge pull request #356 from outis1one/claude/gitea-standalone-setup-oxoi2e
Claude/gitea standalone setup oxoi2e
2026-08-17 13:56:20 -04:00
Claude bd6c5d445a gitea-github-sync: fix set -e aborting do_sync on the first successful repo
((pull_count++)) evaluates to the PRE-increment value — 0 on the very
first successful pull/push — and under this script's `set -euo pipefail`,
an arithmetic command evaluating to 0 counts as a failing command and
kills the script immediately. Confirmed live: a real, fully successful
GitHub -> Gitea pull (visible in sync.log as "PULL ... OK") still made
the whole run exit non-zero and get reported as "Sync run failed", purely
because it was the first repo to sync (0 -> 1). Any subsequent repo in
the same run would have been fine, but most real installs only have a
handful of repos, so this could look like sync is just broken.

Switched all three counters (pull_count, push_count, fail_count) to
assignment form (`count=$((count + 1))`), which always exits 0 regardless
of the resulting value. page++ elsewhere in the file starts at 1, not 0,
so it isn't affected by this and was left as-is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 17:52:13 +00:00
Claude eae02da074 gitea-github-sync: show the actual HTTP response on a failed user check
"Failed to reach GitHub API. Check GITHUB_TOKEN." (and the equivalent
Gitea message) pointed at the token every time, even when the real cause
was something else entirely — confirmed live twice in one debugging
session: once a GitHub-side 503 outage, once a Gitea account locked
behind a must-change-password 403. Both times the fix was to run the
same curl by hand to see the actual status/response.

Fold that same probe into the script itself: on failure, re-request with
-i and print the HTTP status and response body directly, so the failure
mode (bad token vs. remote outage vs. account lock vs. network/DNS) is
visible immediately instead of requiring a manual curl round-trip.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 17:48:19 +00:00
Outis 4b694d0bef Merge pull request #355 from outis1one/claude/gitea-standalone-setup-oxoi2e
gitea: don't leave the admin account locked behind a password-change …
2026-08-17 13:47:36 -04:00
Claude 67ee2fc28b gitea: don't leave the admin account locked behind a password-change wall
Root cause of the "Failed to reach Gitea API" / 403 errors on every retry:
`gitea admin user change-password` (used in the already-exists branch to
sync the account's password to what the user just entered) defaults to
setting must_change_password=true, unlike `user create` which was already
pinned to --must-change-password=false. Once set, Gitea rejects every API
call — including the sync script's own token-authenticated calls — with
403 "You must change your password", even though the token itself and
GITEA_URL were both completely correct. Confirmed live via a direct curl
against /api/v1/user.

Pin the same flag on change-password that create already used.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 17:41:02 +00:00
Outis 9e88f7c741 Merge pull request #354 from outis1one/claude/gitea-standalone-setup-oxoi2e
gitea: retry token prompts instead of accepting one empty answer
2026-08-17 13:28:48 -04:00
Claude f0ef647f60 gitea: retry token prompts instead of accepting one empty answer
Live symptom: pasting the GitHub PAT over SSH showed the token text
landing on the terminal *after* "No GitHub token entered" had already
printed — the prompt's read() returned empty a beat before the paste
actually arrived (a paste/Enter race that isn't specific to this box,
just common over higher-latency SSH sessions). A single empty answer
was treated as "user has no token" and the install moved on silently.

Both token prompts (GitHub token, and the Gitea-token manual fallback)
now retry up to 3 times interactively before giving up, and strip
whitespace from what was captured in case the paste carried a stray
leading/trailing newline. Unattended installs still take one shot, same
as before, since nobody's there to retry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 17:27:56 +00:00
Outis 49c00e367a Merge pull request #353 from outis1one/claude/gitea-standalone-setup-oxoi2e
Claude/gitea standalone setup oxoi2e
2026-08-17 13:23:36 -04:00
Claude dcf643cde4 gitea: offer to run the sync immediately after configuring it
The sync direction step only ever set up the timer (or printed manual
instructions) — there was no way to actually confirm tokens/config work
without waiting for the first scheduled run or invoking the script by
hand afterward. Add a post-configure prompt: dry-run preview (--list),
run for real right now, or skip. Defaults to dry-run interactively;
defaults to skip under UNATTENDED so a headless install with no GitHub
token configured doesn't spam preflight errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 17:22:27 +00:00
Claude def70ab4e2 gitea: fix token-name collision that swallowed the GitHub token prompt
generate-access-token used a fixed --token-name "sync", which Gitea
rejects on a second call for the same user (e.g. a retry against an
already-existing admin account, now common after the readiness-wait
fix). The failure was silent: it fell through to a manually-labeled
"Paste the Gitea token here" prompt appearing immediately before the
real "GitHub token:" prompt, so a pasted GitHub PAT could land on the
wrong prompt and leave GITHUB_TOKEN empty with no clear reason why.

Token name now includes a timestamp so it's always unique, and the
fallback prompt is relabeled to make clear it wants a Gitea token, not
the GitHub one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 17:19:01 +00:00
Outis a06784e669 Merge pull request #352 from outis1one/claude/gitea-standalone-setup-oxoi2e
gitea: prompt for admin username/password, make readiness wait resilient
2026-08-17 13:12:19 -04:00
2 changed files with 102 additions and 10 deletions
+66 -4
View File
@@ -127,6 +127,26 @@ _gitea_sync_vendor_src() {
echo "$(cd "$_self_dir/.." && pwd)/vendor/ai-stack/gitea-github-sync.sh" echo "$(cd "$_self_dir/.." && pwd)/vendor/ai-stack/gitea-github-sync.sh"
} }
# Prompt for a token with retries — pasted tokens over SSH sometimes race the
# prompt (the terminal delivers the paste a beat after an already-submitted
# empty line, so the token shows up echoed on the *next* line instead of
# being read). A single empty answer used to be taken as "no token", silently
# — this gives it up to 3 tries before actually giving up, and strips
# whitespace in case the paste carried a stray leading/trailing newline.
_gitea_prompt_token() {
local _question="$1" _varname="$2"
local _max=1 _tries=0 _val=""
[ "$UNATTENDED" != true ] && _max=3
while [[ $_tries -lt $_max ]]; do
prompt_text "$_question" "" _val
_val="$(printf '%s' "$_val" | tr -d '[:space:]')"
[[ -n "$_val" ]] && break
_tries=$((_tries + 1))
[[ $_tries -lt $_max ]] && log_warning " Nothing came through — if you pasted it, try again (a paste can race the prompt over SSH)."
done
eval "$_varname='$_val'"
}
# ── Own systemd timer, not gitea-github-sync.sh's built-in --install-timer ── # ── Own systemd timer, not gitea-github-sync.sh's built-in --install-timer ──
# The vendor script's own timer installer always runs the script bare (no # The vendor script's own timer installer always runs the script bare (no
# --pull-only/--push-only), i.e. always both directions — there's no way to # --pull-only/--push-only), i.e. always both directions — there's no way to
@@ -216,6 +236,34 @@ _gitea_run_sync_direction_step() {
log_info " cd $DIR && bash gitea-github-sync.sh $FLAG" log_info " cd $DIR && bash gitea-github-sync.sh $FLAG"
[[ -z "$FLAG" ]] && log_info " (no flag needed for both directions)" [[ -z "$FLAG" ]] && log_info " (no flag needed for both directions)"
fi fi
# ── Run it now, off the timer — lets you confirm tokens/config are
# actually correct right here instead of waiting for the first
# scheduled run (or a manual invocation later) to find out.
echo ""
echo " Run a sync now?"
echo " 1) Dry-run preview only (--list) — shows what would sync, no changes"
echo " 2) Run for real now ($DIR_DESC)"
echo " 3) Skip — don't run anything now"
local _RUN_DEFAULT="1"
[ "$UNATTENDED" = true ] && _RUN_DEFAULT="3"
local _RUN_NOW=""
prompt_text " Choice [$_RUN_DEFAULT]:" "$_RUN_DEFAULT" _RUN_NOW
case "$_RUN_NOW" in
2)
log_info "Running sync now ($DIR_DESC)..."
sudo -u "$ACTUAL_USER" env HOME="$ACTUAL_HOME" SYNC_ENV="$DIR/.env" \
bash "$DIR/gitea-github-sync.sh" $FLAG \
&& log_success "Sync run complete." \
|| log_warning "Sync run failed — check the output above, or ~/.config/gitea-github-sync/sync.log"
;;
3) log_info "Skipped — run it later with the commands above." ;;
*)
log_info "Dry-run preview (--list)..."
sudo -u "$ACTUAL_USER" env HOME="$ACTUAL_HOME" SYNC_ENV="$DIR/.env" \
bash "$DIR/gitea-github-sync.sh" --list
;;
esac
} }
install_gitea() { install_gitea() {
@@ -233,6 +281,7 @@ install_gitea() {
echo "[DRY-RUN] Would prompt for a GitHub token and copy in gitea-github-sync.sh" echo "[DRY-RUN] Would prompt for a GitHub token and copy in gitea-github-sync.sh"
echo "[DRY-RUN] Would ask sync direction (GitHub->Gitea / Gitea->GitHub / both) and whether" echo "[DRY-RUN] Would ask sync direction (GitHub->Gitea / Gitea->GitHub / both) and whether"
echo "[DRY-RUN] to install a systemd timer for automatic sync, or print manual instructions" echo "[DRY-RUN] to install a systemd timer for automatic sync, or print manual instructions"
echo "[DRY-RUN] Would offer to run a sync now (dry-run preview or for real), off-schedule"
echo "[DRY-RUN] Would write $DIR/README.md" echo "[DRY-RUN] Would write $DIR/README.md"
return 0 return 0
fi fi
@@ -337,8 +386,16 @@ EOF
# what was just entered rather than failing the whole install. # what was just entered rather than failing the whole install.
if docker exec -u git gitea gitea admin user list 2>/dev/null | awk '{print $2}' | grep -qx "$GITEA_ADMIN_USER"; then if docker exec -u git gitea gitea admin user list 2>/dev/null | awk '{print $2}' | grep -qx "$GITEA_ADMIN_USER"; then
_exists=true _exists=true
# --must-change-password=false matters here: change-password
# defaults to setting that flag TRUE, which then makes Gitea
# reject every API call (including this script's own token-based
# calls) with 403 "You must change your password" until someone
# logs into the web UI and clears it by hand. Confirmed live —
# this silently broke the sync script on every retry against an
# already-existing account.
docker exec -u git gitea gitea admin user change-password \ docker exec -u git gitea gitea admin user change-password \
--username "$GITEA_ADMIN_USER" --password "$GITEA_ADMIN_PASS" &>/dev/null --username "$GITEA_ADMIN_USER" --password "$GITEA_ADMIN_PASS" \
--must-change-password=false &>/dev/null
_created=true _created=true
break break
fi fi
@@ -356,15 +413,20 @@ EOF
log_success "Admin account created: $GITEA_ADMIN_USER" log_success "Admin account created: $GITEA_ADMIN_USER"
fi fi
# Token name includes a timestamp so a retry against an account that
# already has a "sync" token from an earlier partial run (see the
# already-exists branch above) never collides — Gitea rejects a second
# token with a name that's already taken for that user, which used to
# silently fall through to the manual-paste prompt below on every retry.
local GITEA_TOKEN="" local GITEA_TOKEN=""
GITEA_TOKEN="$(docker exec -u git gitea gitea admin user generate-access-token \ GITEA_TOKEN="$(docker exec -u git gitea gitea admin user generate-access-token \
--username "$GITEA_ADMIN_USER" --token-name sync \ --username "$GITEA_ADMIN_USER" --token-name "sync-$(date +%s)" \
--scopes write:repository,write:user --raw 2>/dev/null)" --scopes write:repository,write:user --raw 2>/dev/null)"
if [[ -z "$GITEA_TOKEN" ]]; then if [[ -z "$GITEA_TOKEN" ]]; then
log_warning "Automatic token generation didn't work (older Gitea image?) — generate one" log_warning "Automatic token generation didn't work (older Gitea image?) — generate one"
log_warning "by hand: log into http://localhost:${WEB_PORT} as $GITEA_ADMIN_USER, then" log_warning "by hand: log into http://localhost:${WEB_PORT} as $GITEA_ADMIN_USER, then"
log_warning "Settings -> Applications -> Generate New Token (repo + user write access)." log_warning "Settings -> Applications -> Generate New Token (repo + user write access)."
prompt_text " Paste the Gitea token here:" "" GITEA_TOKEN _gitea_prompt_token " Paste the GITEA token here (not the GitHub one — that's next):" GITEA_TOKEN
fi fi
# ── GitHub token ───────────────────────────────────────────────────────── # ── GitHub token ─────────────────────────────────────────────────────────
@@ -373,7 +435,7 @@ EOF
log_info "REST API too, which SSH can't do). Generate one at https://github.com/settings/tokens" log_info "REST API too, which SSH can't do). Generate one at https://github.com/settings/tokens"
log_info "with 'repo' scope if you don't already have one handy." log_info "with 'repo' scope if you don't already have one handy."
local GITHUB_TOKEN="" local GITHUB_TOKEN=""
prompt_text " GitHub token:" "" GITHUB_TOKEN _gitea_prompt_token " GitHub token:" GITHUB_TOKEN
if [[ -z "$GITHUB_TOKEN" ]]; then if [[ -z "$GITHUB_TOKEN" ]]; then
log_warning "No GitHub token entered — Gitea itself is still up, but the sync script won't" log_warning "No GitHub token entered — Gitea itself is still up, but the sync script won't"
log_warning "work until you add one to $DIR/.env and re-run this installer (update mode)." log_warning "work until you add one to $DIR/.env and re-run this installer (update mode)."
+36 -6
View File
@@ -87,6 +87,26 @@ _github_api() {
_log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; } _log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }
# On a failed API call, "check your token" is often wrong — it could just as
# easily be the remote service down, an account locked behind a
# must-change-password wall, or a network/DNS problem. Re-probe with -i so
# the actual HTTP status and response body are visible, instead of leaving
# the user to run curl by hand to find out which one it actually was.
_probe_and_report() {
local _what="$1" _url="$2"; shift 2
local _tmp _code _body
_tmp="$(mktemp)"
_code="$(curl -sS -o "$_tmp" -w '%{http_code}' "$@" "$_url" 2>/dev/null)"
_body="$(cat "$_tmp" 2>/dev/null)"
rm -f "$_tmp"
if [[ -z "$_code" || "$_code" == "000" ]]; then
warn " $_what: no HTTP response at all — network/DNS problem reaching $_url, not a credentials problem."
else
warn " $_what responded: HTTP $_code"
[[ -n "$_body" ]] && warn " ${_body:0:300}"
fi
}
# ── config management ────────────────────────────────────────────────────── # ── config management ──────────────────────────────────────────────────────
load_config() { load_config() {
mkdir -p "$CONFIG_DIR" "$WORK_DIR" mkdir -p "$CONFIG_DIR" "$WORK_DIR"
@@ -155,12 +175,22 @@ do_init() {
# Discover usernames # Discover usernames
info "Detecting GitHub user..." info "Detecting GitHub user..."
GITHUB_USER=$(_github_api GET /user | python3 -c "import sys,json; print(json.load(sys.stdin)['login'])" 2>/dev/null) \ GITHUB_USER=$(_github_api GET /user | python3 -c "import sys,json; print(json.load(sys.stdin)['login'])" 2>/dev/null) \
|| { err "Failed to reach GitHub API. Check GITHUB_TOKEN."; exit 1; } || {
err "Failed to reach GitHub API. Check GITHUB_TOKEN."
_probe_and_report "GitHub" "https://api.github.com/user" \
-H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github+json"
exit 1
}
ok "GitHub user: $GITHUB_USER" ok "GitHub user: $GITHUB_USER"
info "Detecting Gitea user..." info "Detecting Gitea user..."
GITEA_USER=$(_gitea_api GET /user | python3 -c "import sys,json; print(json.load(sys.stdin)['login'])" 2>/dev/null) \ GITEA_USER=$(_gitea_api GET /user | python3 -c "import sys,json; print(json.load(sys.stdin)['login'])" 2>/dev/null) \
|| { err "Failed to reach Gitea API. Check GITEA_TOKEN and GITEA_URL ($GITEA_URL)."; exit 1; } || {
err "Failed to reach Gitea API. Check GITEA_TOKEN and GITEA_URL ($GITEA_URL)."
_probe_and_report "Gitea" "$GITEA_URL/api/v1/user" \
-H "Authorization: token $GITEA_TOKEN"
exit 1
}
ok "Gitea user: $GITEA_USER" ok "Gitea user: $GITEA_USER"
# Ask about sync scope # Ask about sync scope
@@ -351,9 +381,9 @@ do_sync() {
[[ -n "$SINGLE_REPO" && "$name" != "$SINGLE_REPO" ]] && continue [[ -n "$SINGLE_REPO" && "$name" != "$SINGLE_REPO" ]] && continue
is_excluded "$name" && continue is_excluded "$name" && continue
if sync_github_to_gitea "$name" "$url" "$priv"; then if sync_github_to_gitea "$name" "$url" "$priv"; then
((pull_count++)) pull_count=$((pull_count + 1))
else else
((fail_count++)) fail_count=$((fail_count + 1))
fi fi
done < <(get_github_repos) done < <(get_github_repos)
fi fi
@@ -372,9 +402,9 @@ do_sync() {
continue continue
fi fi
if sync_gitea_to_github "$name" "$url" "$priv"; then if sync_gitea_to_github "$name" "$url" "$priv"; then
((push_count++)) push_count=$((push_count + 1))
else else
((fail_count++)) fail_count=$((fail_count + 1))
fi fi
done < <(get_gitea_repos) done < <(get_gitea_repos)
fi fi