sync: BOWEI_US 2026-09-02
This commit is contained in:
@@ -17,6 +17,27 @@ CODEX_SKILLS_DIR = C:\Users\<user>\.codex\skills
|
|||||||
凭据说明:首次 clone 成功后运行 `git config --global credential.helper manager`,
|
凭据说明:首次 clone 成功后运行 `git config --global credential.helper manager`,
|
||||||
让 Windows 凭据管理器接管,此后 `SKILLS_REPO_PASS` 可以清空。
|
让 Windows 凭据管理器接管,此后 `SKILLS_REPO_PASS` 可以清空。
|
||||||
|
|
||||||
|
### gitea 服务端(tools-sync 的 gitea_doctor 用;不自建 gitea 就整节留空)
|
||||||
|
|
||||||
|
```
|
||||||
|
GITEA_SSH_HOST = <gitea 宿主机地址,同网段/VPN 内的直连地址优先>
|
||||||
|
GITEA_SSH_PORT = 22
|
||||||
|
GITEA_SSH_HOST_ALT = <走公网跳板时的地址,没有就留空>
|
||||||
|
GITEA_SSH_PORT_ALT = <跳板端口,没有就留空>
|
||||||
|
GITEA_SSH_USER = <登录用户>
|
||||||
|
GITEA_HOST_KEY_FP = <SHA256:... 主机公钥指纹,用于确认连对了机器>
|
||||||
|
GITEA_CONTAINER = gitea
|
||||||
|
GITEA_DOCKER_BIN = /usr/local/bin/docker
|
||||||
|
GITEA_DATA_DIR = <宿主机上 bind 到容器 /data 的目录>
|
||||||
|
GITEA_COMPOSE_FILE = <docker-compose.yml 路径,没用 compose 就留空>
|
||||||
|
GITEA_HTTP_LOCAL = http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
填写要点:脚本要免密登录,先把本机公钥加进服务端的 `authorized_keys` 并实测一次。
|
||||||
|
`GITEA_DOCKER_BIN` 写绝对路径——**ssh 非交互登录通常不加载登录 shell 的 PATH,
|
||||||
|
`docker` 会找不到**。`GITEA_HOST_KEY_FP` 不是可选项:有跳板时它是你唯一能确认
|
||||||
|
"连上的确实是那台机器"的凭据。
|
||||||
|
|
||||||
## Python / Conda
|
## Python / Conda
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# tools-sync: verify the gitea transport from this machine, and when it is broken, log
|
||||||
|
# into the server and repair it.
|
||||||
|
#
|
||||||
|
# Why this exists: on 2026-08-25 the server started handing out empty packs. push kept
|
||||||
|
# working, `ls-remote` kept working, only the pack plane was dead - so every sync run
|
||||||
|
# "succeeded" while three commits sat stranded on one machine for eight days. Checking
|
||||||
|
# the transport is cheap; discovering the outage a week late is not.
|
||||||
|
#
|
||||||
|
# .\gitea_doctor.ps1 # check only, report what is wrong
|
||||||
|
# .\gitea_doctor.ps1 -Fix # check, then repair on the server, then re-verify
|
||||||
|
#
|
||||||
|
# Exit: 0 = transport healthy at the end of the run, 1 = still broken.
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$EnvFile = "$env:USERPROFILE\skills_env.md",
|
||||||
|
[string]$RepoUrl,
|
||||||
|
# Log in and repair, instead of only reporting.
|
||||||
|
[switch]$Fix,
|
||||||
|
# Also allow repairs that can lose data (container recreate, repo surgery). The
|
||||||
|
# server-side script asks for this separately; nothing here needs it by default.
|
||||||
|
[switch]$AllowDestructive,
|
||||||
|
[int]$SshTimeout = 15
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Fail([string]$Msg) { Write-Host "FAIL: $Msg"; exit 1 }
|
||||||
|
|
||||||
|
# ---------- environment ----------
|
||||||
|
if (-not (Test-Path $EnvFile)) { Fail "$EnvFile not found. Copy references\skills_env_template.md there first." }
|
||||||
|
$envText = Get-Content $EnvFile -Raw
|
||||||
|
function Get-EnvKey([string]$Key) {
|
||||||
|
$m = [regex]::Match($envText, "(?m)^\s*$([regex]::Escape($Key))\s*=\s*(.+?)\s*$")
|
||||||
|
if (-not $m.Success) { return "" }
|
||||||
|
$v = $m.Groups[1].Value.Trim()
|
||||||
|
if ($v -match "^<.*>$") { return "" } # unfilled placeholder
|
||||||
|
return $v
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $RepoUrl) { $RepoUrl = Get-EnvKey "SKILLS_REPO_URL" }
|
||||||
|
if (-not $RepoUrl) { Fail "SKILLS_REPO_URL missing from $EnvFile and no -RepoUrl given." }
|
||||||
|
|
||||||
|
$sshHost = Get-EnvKey "GITEA_SSH_HOST"
|
||||||
|
$sshPort = Get-EnvKey "GITEA_SSH_PORT"
|
||||||
|
$sshHostA = Get-EnvKey "GITEA_SSH_HOST_ALT"
|
||||||
|
$sshPortA = Get-EnvKey "GITEA_SSH_PORT_ALT"
|
||||||
|
$sshUser = Get-EnvKey "GITEA_SSH_USER"
|
||||||
|
$container = Get-EnvKey "GITEA_CONTAINER"
|
||||||
|
$dockerBin = Get-EnvKey "GITEA_DOCKER_BIN"
|
||||||
|
if (-not $sshPort) { $sshPort = "22" }
|
||||||
|
if (-not $container) { $container = "gitea" }
|
||||||
|
if (-not $dockerBin) { $dockerBin = "/usr/local/bin/docker" }
|
||||||
|
|
||||||
|
# ---------- client-side transport check ----------
|
||||||
|
# Two planes, because they fail independently and the difference is the whole diagnosis:
|
||||||
|
# refs = ls-remote / info-refs (kept working right through the 2026-09 outage)
|
||||||
|
# pack = an actual fetch (this is what was dead)
|
||||||
|
function Test-Transport {
|
||||||
|
$result = [ordered]@{ Refs = $false; Pack = $false; Sha = ""; Detail = "" }
|
||||||
|
|
||||||
|
$prev = $ErrorActionPreference; $ErrorActionPreference = "Continue"
|
||||||
|
$refs = & git ls-remote $RepoUrl "refs/heads/*" 2>&1 | ForEach-Object { "$_" }
|
||||||
|
$refsCode = $LASTEXITCODE
|
||||||
|
$ErrorActionPreference = $prev
|
||||||
|
if ($refsCode -eq 0 -and $refs) {
|
||||||
|
$result.Refs = $true
|
||||||
|
$result.Sha = (($refs -join "`n") -split "\s+")[0]
|
||||||
|
} else {
|
||||||
|
$result.Detail = ($refs -join " ")
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
# tree:0 + depth 1 keeps this to a few KB; the outage failed even at that size, so a
|
||||||
|
# tiny probe is a valid detector and costs nothing to run on every sync.
|
||||||
|
$tmp = Join-Path ([IO.Path]::GetTempPath()) ("gitea-doctor-" + [guid]::NewGuid().ToString("N"))
|
||||||
|
$prev = $ErrorActionPreference; $ErrorActionPreference = "Continue"
|
||||||
|
$out = & git -c core.askPass=true clone --quiet --bare --depth 1 --filter=tree:0 $RepoUrl $tmp 2>&1 | ForEach-Object { "$_" }
|
||||||
|
$code = $LASTEXITCODE
|
||||||
|
$ErrorActionPreference = $prev
|
||||||
|
if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue }
|
||||||
|
if ($code -eq 0) { $result.Pack = $true } else { $result.Detail = ($out -join " ") }
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "=== gitea transport check: $RepoUrl ==="
|
||||||
|
$t = Test-Transport
|
||||||
|
if ($t.Refs) { Write-Host " refs plane: OK (main-ish head $($t.Sha))" } else { Write-Host " refs plane: BROKEN - $($t.Detail)" }
|
||||||
|
if ($t.Pack) { Write-Host " pack plane: OK" } else { Write-Host " pack plane: BROKEN - $($t.Detail)" }
|
||||||
|
|
||||||
|
if ($t.Refs -and $t.Pack) { Write-Host "transport healthy"; exit 0 }
|
||||||
|
|
||||||
|
# A dead refs plane too means the host or gitea is down/unreachable, not the empty-pack
|
||||||
|
# fault - still worth logging in, the triage script starts by checking the container.
|
||||||
|
if (-not $Fix) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Transport is broken. Re-run with -Fix to log into $sshUser@$sshHost and repair,"
|
||||||
|
Write-Host "or see the skill's Phase 1.5 to do it by hand."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- reach the server ----------
|
||||||
|
if (-not $sshHost -or -not $sshUser) { Fail "GITEA_SSH_HOST / GITEA_SSH_USER missing from $EnvFile - cannot reach the server." }
|
||||||
|
|
||||||
|
function Invoke-Remote([string]$HostName, [string]$Port, [string]$LocalScript, [string[]]$ScriptArgs) {
|
||||||
|
# Copy the script over rather than piping it into `sh -s`. Piping looks simpler but
|
||||||
|
# PS 5.1 encodes native-process stdin its own way and prepends a UTF-8 BOM, which
|
||||||
|
# lands on the shebang and makes sh report "#!/bin/sh: No such file or directory".
|
||||||
|
# scp moves bytes verbatim, so what runs there is exactly the file that is here.
|
||||||
|
$remote = "/tmp/tools-sync-triage.$PID.sh"
|
||||||
|
$prev = $ErrorActionPreference; $ErrorActionPreference = "Continue"
|
||||||
|
$common = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=$SshTimeout", "-o", "StrictHostKeyChecking=accept-new")
|
||||||
|
|
||||||
|
$cp = & scp @common "-P" $Port $LocalScript "${sshUser}@${HostName}:$remote" 2>&1 | ForEach-Object { "$_" }
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
$ErrorActionPreference = $prev
|
||||||
|
return @{ Code = 255; Out = ($cp -join "`n") }
|
||||||
|
}
|
||||||
|
|
||||||
|
$cmd = "sh $remote " + ($ScriptArgs -join " ") + "; rc=`$?; rm -f $remote; exit `$rc"
|
||||||
|
$out = & ssh @common "-p" $Port "$sshUser@$HostName" $cmd 2>&1 | ForEach-Object { "$_" }
|
||||||
|
$code = $LASTEXITCODE
|
||||||
|
$ErrorActionPreference = $prev
|
||||||
|
return @{ Code = $code; Out = ($out -join "`n") }
|
||||||
|
}
|
||||||
|
|
||||||
|
$triage = Join-Path $PSScriptRoot "gitea_triage.sh"
|
||||||
|
if (-not (Test-Path $triage)) { Fail "gitea_triage.sh missing next to this script." }
|
||||||
|
# Normalise to LF and no BOM before shipping it: a CRLF or BOM on the shebang line makes
|
||||||
|
# sh fail with a "No such file or directory" that names the interpreter, not the cause.
|
||||||
|
$triageStaged = Join-Path ([IO.Path]::GetTempPath()) "tools-sync-triage-$PID.sh"
|
||||||
|
$triageText = ((Get-Content $triage -Raw) -replace "`r`n", "`n") -replace "^", ""
|
||||||
|
[IO.File]::WriteAllText($triageStaged, $triageText, (New-Object System.Text.UTF8Encoding($false)))
|
||||||
|
|
||||||
|
$args = @("--container", $container, "--docker", $dockerBin, "--fix")
|
||||||
|
if ($AllowDestructive) { $args += "--allow-destructive" }
|
||||||
|
|
||||||
|
$targets = @(@{ H = $sshHost; P = $sshPort; Label = "direct" })
|
||||||
|
if ($sshHostA -and $sshPortA) { $targets += @{ H = $sshHostA; P = $sshPortA; Label = "fallback" } }
|
||||||
|
|
||||||
|
$r = $null
|
||||||
|
foreach ($tg in $targets) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== server triage via $($tg.Label): $sshUser@$($tg.H):$($tg.P) ==="
|
||||||
|
$r = Invoke-Remote $tg.H $tg.P $triageStaged $args
|
||||||
|
if ($r.Out -notmatch "Connection (timed out|refused)|Could not resolve|Permission denied|No route to host|lost connection") { break }
|
||||||
|
Write-Host " unreachable: $(($r.Out -split "`n")[0])"
|
||||||
|
$r = $null
|
||||||
|
}
|
||||||
|
Remove-Item $triageStaged -Force -ErrorAction SilentlyContinue
|
||||||
|
if (-not $r) { Fail "could not reach the gitea host on any configured path (see GITEA_SSH_HOST / _ALT)." }
|
||||||
|
|
||||||
|
Write-Host $r.Out
|
||||||
|
|
||||||
|
# ---------- re-verify from here ----------
|
||||||
|
# The server saying "repaired" is a claim; the client completing a fetch is the evidence.
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== re-verifying transport from this machine ==="
|
||||||
|
$t2 = Test-Transport
|
||||||
|
if ($t2.Refs -and $t2.Pack) {
|
||||||
|
Write-Host "transport healthy again - re-run the sync."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
if ($t2.Refs) { Write-Host " refs plane: OK" } else { Write-Host " refs plane: BROKEN - $($t2.Detail)" }
|
||||||
|
if ($t2.Pack) { Write-Host " pack plane: OK" } else { Write-Host " pack plane: BROKEN - $($t2.Detail)" }
|
||||||
|
Fail "server triage ran but the transport is still broken. Read the RESULT line above; if it says still-broken, the fault is outside the known repairs - escalate to the skill's Phase 1.5 ladder."
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# tools-sync: gitea server-side triage + repair. Runs ON the gitea host (POSIX sh).
|
||||||
|
#
|
||||||
|
# Driven by gitea_doctor.ps1, which pipes this in over ssh. Safe to run by hand too.
|
||||||
|
#
|
||||||
|
# Every check prints "PASS <id> <detail>" or "FAIL <id> <detail>" so the caller can
|
||||||
|
# parse the outcome without re-deriving it. Repairs only happen with --fix, and only
|
||||||
|
# the reversible ones; anything that could lose data needs --allow-destructive.
|
||||||
|
#
|
||||||
|
# Exit: 0 = healthy (after any repairs), 1 = still broken, 2 = usage/precondition error.
|
||||||
|
|
||||||
|
CONTAINER=gitea
|
||||||
|
DOCKER=/usr/local/bin/docker
|
||||||
|
FIX=0
|
||||||
|
DESTRUCTIVE=0
|
||||||
|
REPO=""
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--container) CONTAINER="$2"; shift 2 ;;
|
||||||
|
--docker) DOCKER="$2"; shift 2 ;;
|
||||||
|
--repo) REPO="$2"; shift 2 ;;
|
||||||
|
--fix) FIX=1; shift ;;
|
||||||
|
--allow-destructive) DESTRUCTIVE=1; shift ;;
|
||||||
|
*) echo "FAIL usage unknown argument: $1"; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -x "$DOCKER" ] || { echo "FAIL docker $DOCKER not executable (ssh PATH is not the login PATH)"; exit 2; }
|
||||||
|
D="$DOCKER"
|
||||||
|
TS=$(date +%Y%m%d-%H%M%S)
|
||||||
|
BROKEN=0
|
||||||
|
REPAIRED=""
|
||||||
|
|
||||||
|
note() { echo "PASS $1 $2"; }
|
||||||
|
bad() { echo "FAIL $1 $2"; BROKEN=1; }
|
||||||
|
fixed() { echo "FIXED $1 $2"; REPAIRED="$REPAIRED $1"; }
|
||||||
|
dex() { $D exec "$CONTAINER" sh -c "$1" 2>&1; }
|
||||||
|
|
||||||
|
echo "=== gitea triage $TS (container=$CONTAINER fix=$FIX destructive=$DESTRUCTIVE) ==="
|
||||||
|
|
||||||
|
# ---------- A. container is up ----------
|
||||||
|
STATUS=$($D inspect "$CONTAINER" --format '{{.State.Status}}' 2>/dev/null)
|
||||||
|
if [ -z "$STATUS" ]; then
|
||||||
|
bad container "no such container: $CONTAINER"
|
||||||
|
echo "--- containers present ---"; $D ps -a --format '{{.Names}} {{.Image}} {{.Status}}'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$STATUS" != "running" ]; then
|
||||||
|
bad container "status=$STATUS"
|
||||||
|
if [ "$FIX" = 1 ]; then
|
||||||
|
$D start "$CONTAINER" >/dev/null 2>&1 && sleep 8
|
||||||
|
STATUS=$($D inspect "$CONTAINER" --format '{{.State.Status}}' 2>/dev/null)
|
||||||
|
[ "$STATUS" = "running" ] && fixed container "started" || { echo "FAIL container could not start"; exit 1; }
|
||||||
|
else
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
note container "running ($($D inspect "$CONTAINER" --format '{{.Config.Image}}'))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Where gitea's own HOME (and therefore its global gitconfig) lives. It is NOT /data/git.
|
||||||
|
GHOME=$(dex 'getent passwd git 2>/dev/null | cut -d: -f6')
|
||||||
|
[ -n "$GHOME" ] || GHOME=/data/git
|
||||||
|
GCFG=$(dex 'ls /data/gitea/home/.gitconfig >/dev/null 2>&1 && echo /data/gitea/home/.gitconfig || echo '"$GHOME"'/.gitconfig')
|
||||||
|
GCFG=$(echo "$GCFG" | tr -d '\r')
|
||||||
|
echo "INFO gitconfig $GCFG"
|
||||||
|
|
||||||
|
# ---------- B. disk ----------
|
||||||
|
FREE=$(dex 'df -P /data | tail -1 | awk "{print \$4}"')
|
||||||
|
FREE=$(echo "$FREE" | tr -d '\r ')
|
||||||
|
if [ -n "$FREE" ] && [ "$FREE" -lt 1048576 ] 2>/dev/null; then
|
||||||
|
bad disk "only ${FREE}KB free on /data"
|
||||||
|
else
|
||||||
|
note disk "${FREE}KB free on /data"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- C. global gitconfig sanity ----------
|
||||||
|
# The 2026-09-02 outage: [uploadpack] packObjectsHook replaced git pack-objects with a
|
||||||
|
# script that produced no pack, so every clone got an empty one. Any hook-ish config
|
||||||
|
# here is remote code execution by design, so treat its mere presence as a failure.
|
||||||
|
CFG_SIZE=$(dex "wc -c < $GCFG 2>/dev/null" | tr -d '\r ')
|
||||||
|
[ -n "$CFG_SIZE" ] || CFG_SIZE=0
|
||||||
|
HOOKCFG=$(dex "grep -icE 'packObjectsHook|core\\.hooksPath|sshCommand|credential\\.helper *= *!' $GCFG 2>/dev/null" | tr -d '\r ')
|
||||||
|
[ -n "$HOOKCFG" ] || HOOKCFG=0
|
||||||
|
if [ "$HOOKCFG" != 0 ] || [ "$CFG_SIZE" -gt 65536 ] 2>/dev/null; then
|
||||||
|
bad gitconfig "size=${CFG_SIZE}B suspicious-directives=${HOOKCFG} (expected a few hundred B, 0)"
|
||||||
|
dex "grep -inE 'packObjectsHook|core\\.hooksPath|sshCommand' $GCFG | head -5"
|
||||||
|
if [ "$FIX" = 1 ]; then
|
||||||
|
# gitea rewrites a clean global gitconfig at startup, so quarantine + restart is
|
||||||
|
# both the repair and the proof: hand-editing it would only paper over the entry.
|
||||||
|
dex "cp $GCFG /data/gitea/home/.gitconfig.quarantine.$TS 2>/dev/null || cp $GCFG $GCFG.quarantine.$TS"
|
||||||
|
dex "rm -f $GCFG"
|
||||||
|
$D restart "$CONTAINER" >/dev/null 2>&1
|
||||||
|
sleep 12
|
||||||
|
NEW=$(dex "wc -c < $GCFG 2>/dev/null" | tr -d '\r ')
|
||||||
|
NEWHOOK=$(dex "grep -icE 'packObjectsHook|core\\.hooksPath' $GCFG 2>/dev/null" | tr -d '\r ')
|
||||||
|
[ -n "$NEWHOOK" ] || NEWHOOK=0
|
||||||
|
if [ "$NEWHOOK" = 0 ] && [ -n "$NEW" ]; then
|
||||||
|
fixed gitconfig "quarantined to .gitconfig.quarantine.$TS, gitea rebuilt a clean ${NEW}B one"
|
||||||
|
else
|
||||||
|
echo "FAIL gitconfig rebuild did not produce a clean file (size=$NEW hooks=$NEWHOOK)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
note gitconfig "${CFG_SIZE}B, no hook directives"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- D. tampered repo hooks ----------
|
||||||
|
TAMPER=$(dex "grep -rlE '/tmp/[A-Za-z0-9_]+' /data/git/repositories 2>/dev/null | head -20")
|
||||||
|
TAMPER=$(echo "$TAMPER" | tr -d '\r' | sed '/^$/d')
|
||||||
|
if [ -n "$TAMPER" ]; then
|
||||||
|
bad hooks "hook files referencing /tmp:"
|
||||||
|
echo "$TAMPER" | sed 's/^/ /'
|
||||||
|
if [ "$FIX" = 1 ]; then
|
||||||
|
for h in $TAMPER; do
|
||||||
|
base=$(basename "$h")
|
||||||
|
tpl="/usr/share/git-core/templates/hooks/$base"
|
||||||
|
if dex "[ -f $tpl ]" >/dev/null 2>&1; then
|
||||||
|
dex "cp $tpl $h && chmod 755 $h"
|
||||||
|
else
|
||||||
|
dex "grep -v '/tmp/' $h > $h.clean && mv $h.clean $h && chmod 755 $h"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
LEFT=$(dex "grep -rlE '/tmp/[A-Za-z0-9_]+' /data/git/repositories 2>/dev/null | head -5" | tr -d '\r' | sed '/^$/d')
|
||||||
|
[ -z "$LEFT" ] && fixed hooks "restored from git templates" || echo "FAIL hooks still tampered: $LEFT"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
note hooks "no hook references /tmp"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- E. dropped payloads ----------
|
||||||
|
PAYLOAD=$(dex "ls /tmp/giteahook* /tmp/col_* 2>/dev/null | head -20" | tr -d '\r' | sed '/^$/d')
|
||||||
|
if [ -n "$PAYLOAD" ]; then
|
||||||
|
bad payload "executable drops in /tmp:"
|
||||||
|
echo "$PAYLOAD" | sed 's/^/ /'
|
||||||
|
[ "$FIX" = 1 ] && { dex "rm -f /tmp/giteahook* /tmp/col_*" >/dev/null 2>&1; fixed payload "removed"; }
|
||||||
|
else
|
||||||
|
note payload "none"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- F. git binary not shimmed ----------
|
||||||
|
# A leftover diagnostic wrapper (git.real present) silently changes every git call.
|
||||||
|
if dex "[ -e /usr/bin/git.real ]" >/dev/null 2>&1; then
|
||||||
|
bad gitbin "/usr/bin/git.real exists - /usr/bin/git is a wrapper, not the real binary"
|
||||||
|
if [ "$FIX" = 1 ]; then
|
||||||
|
dex "cp /usr/bin/git.real /usr/bin/git && rm -f /usr/bin/git.real" >/dev/null 2>&1
|
||||||
|
fixed gitbin "real git restored"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
note gitbin "$(dex 'git --version' | tr -d '\r')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- G. pack generation (the ladder's decisive rung) ----------
|
||||||
|
# Must use --stateless-rpc: that is the code path HTTP takes. A `clone file://` proves
|
||||||
|
# nothing here, which is exactly how this fault stayed misdiagnosed for a week.
|
||||||
|
if [ -z "$REPO" ]; then
|
||||||
|
REPO=$(dex 'ls -d /data/git/repositories/*/*.git 2>/dev/null | head -1' | tr -d '\r')
|
||||||
|
fi
|
||||||
|
if [ -n "$REPO" ]; then
|
||||||
|
SHA=$(dex "git --git-dir=$REPO rev-parse HEAD 2>/dev/null" | tr -d '\r')
|
||||||
|
if [ -n "$SHA" ]; then
|
||||||
|
BYTES=$(dex "printf '0032want $SHA\n00000009done\n' | git upload-pack --stateless-rpc $REPO 2>/dev/null | wc -c" | tr -d '\r ')
|
||||||
|
[ -n "$BYTES" ] || BYTES=0
|
||||||
|
if [ "$BYTES" -lt 512 ] 2>/dev/null; then
|
||||||
|
bad packgen "upload-pack --stateless-rpc produced only ${BYTES}B for $REPO (a real pack is KBs+)"
|
||||||
|
echo " -> git itself is being told not to work; check the gitconfig directives above"
|
||||||
|
else
|
||||||
|
note packgen "${BYTES}B pack from $REPO"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
bad packgen "cannot resolve HEAD of $REPO"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
bad packgen "no repository found under /data/git/repositories"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- H. end-to-end clone inside the container ----------
|
||||||
|
# Bypasses frp, any reverse proxy and docker port forwarding: a failure here is gitea's.
|
||||||
|
PUB=$(dex 'ls -d /data/git/repositories/*/*.git 2>/dev/null | head -5' | tr -d '\r')
|
||||||
|
CLONE_OK=0
|
||||||
|
CLONE_MSG="no public repo reachable anonymously"
|
||||||
|
for r in $PUB; do
|
||||||
|
owner=$(basename "$(dirname "$r")")
|
||||||
|
name=$(basename "$r" .git)
|
||||||
|
out=$(dex "rm -rf /tmp/tsdoc; GIT_TERMINAL_PROMPT=0 git clone -q --filter=tree:0 --depth 1 http://localhost:3000/$owner/$name.git /tmp/tsdoc 2>&1; ls /tmp/tsdoc/.git >/dev/null 2>&1 && echo TSDOC_OK; rm -rf /tmp/tsdoc")
|
||||||
|
case "$out" in
|
||||||
|
*TSDOC_OK*) CLONE_OK=1; CLONE_MSG="$owner/$name"; break ;;
|
||||||
|
*"could not read Username"*|*"Authentication"*) continue ;;
|
||||||
|
*) CLONE_MSG="$owner/$name: $(echo "$out" | tr -d '\r' | tr '\n' ' ' | cut -c1-160)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ "$CLONE_OK" = 1 ] && note httpclone "$CLONE_MSG" || bad httpclone "$CLONE_MSG"
|
||||||
|
|
||||||
|
# ---------- verdict ----------
|
||||||
|
echo "=== verdict ==="
|
||||||
|
if [ "$BROKEN" = 0 ]; then
|
||||||
|
echo "RESULT healthy"; exit 0
|
||||||
|
fi
|
||||||
|
if [ -n "$REPAIRED" ]; then
|
||||||
|
echo "RESULT repaired:$REPAIRED (caller must re-verify from the client side)"; exit 0
|
||||||
|
fi
|
||||||
|
echo "RESULT still-broken (run with --fix to apply the reversible repairs)"; exit 1
|
||||||
@@ -15,7 +15,12 @@ param(
|
|||||||
# linking; -Force deletes it and links (caller must have rescued unique content first).
|
# linking; -Force deletes it and links (caller must have rescued unique content first).
|
||||||
[switch]$Force,
|
[switch]$Force,
|
||||||
# Report what would happen without committing/pushing/linking.
|
# Report what would happen without committing/pushing/linking.
|
||||||
[switch]$DryRun
|
[switch]$DryRun,
|
||||||
|
# A transport failure normally aborts the run and leaves commits stranded here. By
|
||||||
|
# default we instead log into the gitea host, repair it, and retry once. -NoDoctor
|
||||||
|
# keeps the old abort-and-report behaviour.
|
||||||
|
[switch]$NoDoctor,
|
||||||
|
[switch]$AllowDestructive
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -34,6 +39,28 @@ function Invoke-Git {
|
|||||||
|
|
||||||
function Fail([string]$Msg) { Write-Host "FAIL: $Msg"; exit 1 }
|
function Fail([string]$Msg) { Write-Host "FAIL: $Msg"; exit 1 }
|
||||||
|
|
||||||
|
# git reports a server-side transport fault in the vocabulary of the pack protocol, not
|
||||||
|
# of the server. These are the shapes seen when gitea is up (auth fine, refs fine) but
|
||||||
|
# the pack plane is dead - the case gitea_doctor knows how to repair.
|
||||||
|
function Test-TransportFault([string]$GitOutput) {
|
||||||
|
return ($GitOutput -match "bad pack header|early EOF|invalid index-pack output|RPC failed|unexpected disconnect|remote end hung up|Could not read from remote repository|Connection (reset|refused|timed out)")
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Doctor([string]$Why) {
|
||||||
|
if ($NoDoctor) { Write-Host "(-NoDoctor set, skipping server repair)"; return $false }
|
||||||
|
$doctor = Join-Path $PSScriptRoot "gitea_doctor.ps1"
|
||||||
|
if (-not (Test-Path $doctor)) { Write-Host "gitea_doctor.ps1 not found; cannot repair the server."; return $false }
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== transport fault ($Why) - running gitea_doctor -Fix ==="
|
||||||
|
$dargs = @("-RepoUrl", $RepoUrl, "-Fix")
|
||||||
|
if ($AllowDestructive) { $dargs += "-AllowDestructive" }
|
||||||
|
& $doctor @dargs
|
||||||
|
$ok = ($LASTEXITCODE -eq 0)
|
||||||
|
Write-Host "=== doctor $(if ($ok) { 'reports the transport healthy' } else { 'could NOT restore the transport' }) ==="
|
||||||
|
Write-Host ""
|
||||||
|
return $ok
|
||||||
|
}
|
||||||
|
|
||||||
# ---------- Phase 1: primary checkout (~\.claude\skills) ----------
|
# ---------- Phase 1: primary checkout (~\.claude\skills) ----------
|
||||||
Write-Host "=== [1/2] primary git sync: $ClaudeSkills ==="
|
Write-Host "=== [1/2] primary git sync: $ClaudeSkills ==="
|
||||||
if (-not (Test-Path (Join-Path $ClaudeSkills ".git"))) {
|
if (-not (Test-Path (Join-Path $ClaudeSkills ".git"))) {
|
||||||
@@ -94,16 +121,26 @@ if ($DryRun) {
|
|||||||
Assert-RemoteInSync $ClaudeSkills $Branch
|
Assert-RemoteInSync $ClaudeSkills $Branch
|
||||||
} else {
|
} else {
|
||||||
$r = Invoke-Git $ClaudeSkills @("pull", "--rebase", "origin", $Branch)
|
$r = Invoke-Git $ClaudeSkills @("pull", "--rebase", "origin", $Branch)
|
||||||
|
if ($r.Code -ne 0 -and (Test-TransportFault $r.Out)) {
|
||||||
|
# A rebase may have been left half-started before the transport died.
|
||||||
|
Invoke-Git $ClaudeSkills @("rebase", "--abort") | Out-Null
|
||||||
|
if (Invoke-Doctor "pull --rebase") {
|
||||||
|
$r = Invoke-Git $ClaudeSkills @("pull", "--rebase", "origin", $Branch)
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($r.Code -ne 0) {
|
if ($r.Code -ne 0) {
|
||||||
Write-Host $r.Out
|
Write-Host $r.Out
|
||||||
$stranded = Invoke-Git $ClaudeSkills @("rev-list", "--count", "origin/$Branch..$Branch")
|
$stranded = Invoke-Git $ClaudeSkills @("rev-list", "--count", "origin/$Branch..$Branch")
|
||||||
if ($stranded.Code -eq 0 -and [int]$stranded.Out -gt 0) {
|
if ($stranded.Code -eq 0 -and [int]$stranded.Out -gt 0) {
|
||||||
Write-Host "NOTE: $($stranded.Out) local commit(s) cannot reach the server until this is fixed."
|
Write-Host "NOTE: $($stranded.Out) local commit(s) cannot reach the server until this is fixed."
|
||||||
}
|
}
|
||||||
Fail "pull --rebase failed on primary. Resolve conflicts (or 'git rebase --abort') then re-run. If the transport itself is broken (see skill Phase 1), fix the server -- do not leave commits stranded."
|
Fail "pull --rebase failed on primary. Conflicts are yours to resolve (or 'git rebase --abort'); a transport fault the doctor could not repair is in the skill's Phase 1.5 ladder. Either way: do not leave commits stranded."
|
||||||
}
|
}
|
||||||
Write-Host $r.Out
|
Write-Host $r.Out
|
||||||
$r = Invoke-Git $ClaudeSkills @("push", "origin", $Branch)
|
$r = Invoke-Git $ClaudeSkills @("push", "origin", $Branch)
|
||||||
|
if ($r.Code -ne 0 -and (Test-TransportFault $r.Out)) {
|
||||||
|
if (Invoke-Doctor "push") { $r = Invoke-Git $ClaudeSkills @("push", "origin", $Branch) }
|
||||||
|
}
|
||||||
if ($r.Code -ne 0) { Write-Host $r.Out; Fail "push failed (auth? see skill Phase 1)." }
|
if ($r.Code -ne 0) { Write-Host $r.Out; Fail "push failed (auth? see skill Phase 1)." }
|
||||||
Assert-RemoteInSync $ClaudeSkills $Branch
|
Assert-RemoteInSync $ClaudeSkills $Branch
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user