From 3ffcde7294c4d5b7d1413bcca0e26bcbf9d08a9b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 18:51:53 +0000 Subject: [PATCH 1/2] Fix gitea-github-sync.sh: stop mirroring refs/pull/* into Gitea/GitHub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both sync directions used `git clone --bare` for the first clone and `git push --mirror` for the push. A bare clone pulls in every ref the remote advertises, refs/pull/*/head included — GitHub (and Gitea, same behavior) exposes PR refs over the same smart-HTTP endpoint a plain bare clone reads from. --mirror then pushes every local ref verbatim, including those, and gets rejected: Gitea's server-side hook (and GitHub's own PR-ref protection) reserves that namespace for itself. remote: error: hook declined to update refs/pull/1/head ! [remote rejected] refs/pull/1/head (hook declined) Fix: scope the initial clone (now git init --bare + fetch, unified with the repeat-sync path instead of a separate git-clone branch) and the push to an explicit refs/heads/*:refs/heads/* + refs/tags/*:refs/tags/* refspec in both directions, matching the refspec discipline the fetch side already had. Added a defensive cleanup (delete any refs/pull/*, refs/merge-requests/*, refs/changes/* found in the local bare mirror before pushing) so a repo synced before this fix self-heals on its next run instead of tripping the same hook forever. --- vendor/ai-stack/gitea-github-sync.sh | 62 +++++++++++++++++++++------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/vendor/ai-stack/gitea-github-sync.sh b/vendor/ai-stack/gitea-github-sync.sh index cd7f5b2..c07d78f 100755 --- a/vendor/ai-stack/gitea-github-sync.sh +++ b/vendor/ai-stack/gitea-github-sync.sh @@ -277,17 +277,34 @@ sync_github_to_gitea() { # old commit indefinitely, with no error at any step. Also no longer # silencing stderr: a real auth/network failure should be visible in the # log, not just "Failed to fetch" with no reason why. + local auth_url="${clone_url/https:\/\//https:\/\/$GITHUB_TOKEN@}" if [[ -d "$local_path" ]]; then info "Fetching $full_name from GitHub..." - git -C "$local_path" fetch origin '+refs/heads/*:refs/heads/*' --prune --quiet || { - err "Failed to fetch $full_name"; return 1; } else info "Cloning $full_name from GitHub..." mkdir -p "$(dirname "$local_path")" - local auth_url="${clone_url/https:\/\//https:\/\/$GITHUB_TOKEN@}" - git clone --bare --quiet "$auth_url" "$local_path" || { - err "Failed to clone $full_name"; return 1; } + git init --bare --quiet "$local_path" || { err "Failed to init $full_name"; return 1; } + git -C "$local_path" remote add origin "$auth_url" fi + # Explicit heads+tags refspec on BOTH the initial clone and every later + # fetch, not `git clone --bare` (which pulls every ref the remote + # advertises, refs/pull/*/head included) — GitHub exposes PR refs over + # the same smart-HTTP endpoint a plain bare clone reads from, and those + # live in a namespace Gitea's own PR system reserves for itself. A later + # `git push --mirror` (pushes every local ref verbatim) then gets + # rejected by Gitea's server-side hook — confirmed live: "hook declined + # to update refs/pull/1/head". Scoping fetch AND push to heads/tags only + # avoids ever touching that namespace in either direction. + git -C "$local_path" fetch origin \ + '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' \ + --prune --quiet || { err "Failed to fetch $full_name"; return 1; } + # Self-heals a repo synced before this fix — a stray refs/pull/* (or any + # other non-heads/tags ref) an earlier run's unscoped `clone --bare` + # already pulled in would otherwise keep tripping the same Gitea hook on + # every sync from here on, with no other way to clear it. + git -C "$local_path" for-each-ref --format='%(refname)' \ + 'refs/pull/*' 'refs/merge-requests/*' 'refs/changes/*' \ + | xargs -r -n1 git -C "$local_path" update-ref -d # Ensure repo exists on Gitea local gitea_check @@ -299,12 +316,17 @@ sync_github_to_gitea() { >/dev/null || { err "Failed to create $repo_name on Gitea"; return 1; } fi - # Push to Gitea + # Push to Gitea — same explicit heads+tags scoping as the fetch above, + # not --mirror (which would push refs/pull/* etc. verbatim and hit the + # same rejected-hook failure this whole fix is for). --prune still makes + # Gitea's heads/tags a true mirror of GitHub's (deletes ones GitHub no + # longer has), just without ever touching reserved ref namespaces. local gitea_push_url="${GITEA_URL/https:\/\//https:\/\/$GITEA_USER:$GITEA_TOKEN@}" gitea_push_url="${gitea_push_url/http:\/\//http:\/\/$GITEA_USER:$GITEA_TOKEN@}" gitea_push_url="$gitea_push_url/$GITEA_USER/$repo_name.git" - git -C "$local_path" push --mirror "$gitea_push_url" --quiet || { + git -C "$local_path" push --prune --quiet "$gitea_push_url" \ + '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' || { err "Failed to push $full_name to Gitea"; return 1; } ok "GitHub → Gitea: $full_name" _log "PULL $full_name OK" @@ -320,18 +342,26 @@ sync_gitea_to_github() { local gitea_auth_url="${clone_url/https:\/\//https:\/\/$GITEA_USER:$GITEA_TOKEN@}" gitea_auth_url="${gitea_auth_url/http:\/\//http:\/\/$GITEA_USER:$GITEA_TOKEN@}" - # See the matching comment in sync_github_to_gitea() above — same - # explicit-refspec, visible-stderr fix, same reason. + # See the matching comment in sync_github_to_gitea() above — same reason + # applies in reverse: Gitea also exposes PR refs (refs/pull/*/head) over + # its git smart-HTTP endpoint, and GitHub rejects direct pushes to that + # same reserved namespace just as Gitea's hook does. Explicit heads+tags + # refspec on the initial clone too, not `git clone --bare`. if [[ -d "$local_path" ]]; then info "Fetching $full_name from Gitea..." - git -C "$local_path" fetch origin '+refs/heads/*:refs/heads/*' --prune --quiet || { - err "Failed to fetch $full_name from Gitea"; return 1; } else info "Cloning $full_name from Gitea..." mkdir -p "$(dirname "$local_path")" - git clone --bare --quiet "$gitea_auth_url" "$local_path" || { - err "Failed to clone $full_name from Gitea"; return 1; } + git init --bare --quiet "$local_path" || { err "Failed to init $full_name"; return 1; } + git -C "$local_path" remote add origin "$gitea_auth_url" fi + git -C "$local_path" fetch origin \ + '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' \ + --prune --quiet || { err "Failed to fetch $full_name from Gitea"; return 1; } + # Self-heals a repo synced before this fix — see the matching comment above. + git -C "$local_path" for-each-ref --format='%(refname)' \ + 'refs/pull/*' 'refs/merge-requests/*' 'refs/changes/*' \ + | xargs -r -n1 git -C "$local_path" update-ref -d # Ensure repo exists on GitHub local gh_check @@ -343,9 +373,11 @@ sync_gitea_to_github() { >/dev/null || { err "Failed to create $repo_name on GitHub"; return 1; } fi - # Push to GitHub + # Push to GitHub — explicit heads+tags scoping, not --mirror. Same + # reasoning as the Gitea push above. local github_push_url="https://$GITHUB_TOKEN@github.com/$GITHUB_USER/$repo_name.git" - git -C "$local_path" push --mirror "$github_push_url" --quiet || { + git -C "$local_path" push --prune --quiet "$github_push_url" \ + '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' || { err "Failed to push $full_name to GitHub"; return 1; } ok "Gitea → GitHub: $full_name" _log "PUSH $full_name OK" From 76a494bcbfa977e60c6c31f7ce29ff2ceb32a727 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 20:09:33 +0000 Subject: [PATCH 2/2] Fix wolf.sh: mount whole retroarch/ dir, not just its subdirectories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The esde and retroarch app profiles (both CATALOG dict copies) mounted three subdirectories individually: {games}/retroarch/cores:/home/retro/.config/retroarch/cores:rw {games}/retroarch/shaders:/home/retro/.config/retroarch/shaders:rw {games}/retroarch/overlays:/home/retro/.config/retroarch/overlays:rw but never mounted /home/retro/.config/retroarch itself. Docker auto-creates that missing parent directory inside the container as root:root mode 755 (standard behavior for a bind-mount target that doesn't already exist in the image) — the retro user (uid 1000) can read/traverse it but not write into it. RetroArch's own entrypoint then fails outright trying to write its default config there: cp: cannot create regular file '/home/retro/.config/retroarch/retroarch.cfg': Permission denied which happens on every single launch, for both apps — confirmed live against a real box: the container starts, RetroArch dies on that cp within ~1s, and Wolf tears the session down (the "black screen, back to app grid" symptom, with nothing RetroArch-specific about it). Fix: mount the parent {games}/retroarch directory itself onto /home/retro/.config/retroarch instead of three separate subdirectory mounts. cores/shaders/overlays already lived as the only subdirectories under {games}/retroarch/ on the host, so this preserves the exact same container-side paths — but now the parent is a real bind mount with no auto-created stub in the way, and RetroArch's other generated config (button remaps, core options, playlists, cheats, etc.) persists across sessions too, which the old three-mount setup silently discarded. --- services/wolf.sh | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/services/wolf.sh b/services/wolf.sh index 463f95b..4b4251b 100644 --- a/services/wolf.sh +++ b/services/wolf.sh @@ -1310,9 +1310,7 @@ CATALOG = { f'{games}/saves:/mnt/games/saves:rw', f'{games}/media:/media:rw', f'{games}/bios:/home/retro/bioses:rw', - f'{games}/retroarch/cores:/home/retro/.config/retroarch/cores:rw', - f'{games}/retroarch/shaders:/home/retro/.config/retroarch/shaders:rw', - f'{games}/retroarch/overlays:/home/retro/.config/retroarch/overlays:rw', + f'{games}/retroarch:/home/retro/.config/retroarch:rw', f'{games}/emulators:/mnt/games/emulators:rw', f'{games}/emulators:/home/retro/Applications:rw'], env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host', @@ -1336,9 +1334,7 @@ CATALOG = { mounts=[f'{games}/roms:/ROMs:rw', f'{games}/saves:/mnt/games/saves:rw', f'{games}/bios:/home/retro/bioses:rw', - f'{games}/retroarch/cores:/home/retro/.config/retroarch/cores:rw', - f'{games}/retroarch/shaders:/home/retro/.config/retroarch/shaders:rw', - f'{games}/retroarch/overlays:/home/retro/.config/retroarch/overlays:rw'], + f'{games}/retroarch:/home/retro/.config/retroarch:rw'], env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host', ulimits=[], privileged=False, ), @@ -2598,9 +2594,7 @@ CATALOG = { f'{games}/saves:/mnt/games/saves:rw', f'{games}/media:/media:rw', f'{games}/bios:/home/retro/bioses:rw', - f'{games}/retroarch/cores:/home/retro/.config/retroarch/cores:rw', - f'{games}/retroarch/shaders:/home/retro/.config/retroarch/shaders:rw', - f'{games}/retroarch/overlays:/home/retro/.config/retroarch/overlays:rw', + f'{games}/retroarch:/home/retro/.config/retroarch:rw', f'{games}/emulators:/mnt/games/emulators:rw', f'{games}/emulators:/home/retro/Applications:rw'], env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host', @@ -2624,9 +2618,7 @@ CATALOG = { mounts=[f'{games}/roms:/ROMs:rw', f'{games}/saves:/mnt/games/saves:rw', f'{games}/bios:/home/retro/bioses:rw', - f'{games}/retroarch/cores:/home/retro/.config/retroarch/cores:rw', - f'{games}/retroarch/shaders:/home/retro/.config/retroarch/shaders:rw', - f'{games}/retroarch/overlays:/home/retro/.config/retroarch/overlays:rw'], + f'{games}/retroarch:/home/retro/.config/retroarch:rw'], env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host', ulimits=[], privileged=False, ),