文件
skills/codex-task-server-sync/scripts/sync_once.sh
2026-03-16 23:46:45 -07:00

218 行
5.6 KiB
Bash
可执行文件

#!/usr/bin/env bash
set -euo pipefail
project_dir=""
host_label=""
usage() {
cat <<'EOF'
Usage: sync-once.sh --project-dir <path> [--host-label <label>]
Synchronize one git-backed Codex workspace against its configured origin:
- autosave dirty work into a host-stamped commit
- fetch origin
- rebase onto the configured branch
- push
- retry once after a non-fast-forward rejection
- stop and surface runtime status on conflicts
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--project-dir)
project_dir="$2"
shift 2
;;
--host-label)
host_label="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$project_dir" ]]; then
script_dir="$(cd "$(dirname "$0")" && pwd)"
project_dir="$(cd "$script_dir/../.." && pwd)"
fi
project_dir="$(cd "$project_dir" && pwd)"
host_label="${host_label:-$(hostname -s 2>/dev/null || hostname)}"
manifest_path="$project_dir/.codex-sync/manifest.json"
runtime_dir="$project_dir/.codex-sync/runtime"
status_path="$runtime_dir/status.json"
error_log="$runtime_dir/last-error.log"
blocked_path="$runtime_dir/blocked"
lock_dir="$runtime_dir/lock"
mkdir -p "$runtime_dir"
write_status() {
local state="$1"
local message="$2"
python3 - "$status_path" "$state" "$host_label" "$message" "$project_dir" <<'PY'
import json
import sys
from datetime import datetime, timezone
path, state, host_label, message, project_dir = sys.argv[1:]
payload = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"state": state,
"host": host_label,
"projectDir": project_dir,
"message": message,
}
with open(path, "w", encoding="utf-8") as handle:
json.dump(payload, handle, indent=2)
handle.write("\n")
PY
}
read_manifest_field() {
local expression="$1"
python3 - "$manifest_path" "$expression" <<'PY'
import json
import sys
path = sys.argv[1]
expr = sys.argv[2]
data = json.load(open(path, "r", encoding="utf-8"))
value = data
for part in expr.split("."):
value = value.get(part, "") if isinstance(value, dict) else ""
print(value if value is not None else "")
PY
}
abort_rebase_if_needed() {
if [[ -d "$project_dir/.git/rebase-apply" || -d "$project_dir/.git/rebase-merge" ]]; then
git -C "$project_dir" rebase --abort >>"$error_log" 2>&1 || true
fi
}
block_sync() {
local reason="$1"
abort_rebase_if_needed
{
printf 'timestamp=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf 'host=%s\n' "$host_label"
printf 'reason=%s\n' "$reason"
} >"$blocked_path"
write_status "blocked" "$reason"
exit 1
}
run_git() {
git -C "$project_dir" "$@" >>"$error_log" 2>&1
}
if [[ ! -f "$manifest_path" ]]; then
write_status "error" "Missing manifest at $manifest_path"
echo "Missing manifest at $manifest_path" >"$error_log"
exit 1
fi
if ! mkdir "$lock_dir" 2>/dev/null; then
write_status "skipped" "Another sync run is already in progress"
exit 0
fi
trap 'rmdir "$lock_dir" 2>/dev/null || true' EXIT
if [[ -f "$blocked_path" ]]; then
write_status "blocked" "Sync is blocked until .codex-sync/runtime/blocked is cleared"
exit 0
fi
: >"$error_log"
write_status "running" "Synchronizing workspace"
branch="$(read_manifest_field branch)"
git_name="$(read_manifest_field gitIdentity.name)"
git_email="$(read_manifest_field gitIdentity.email)"
branch="${branch:-main}"
git_name="${git_name:-Codex Sync}"
git_email="${git_email:-codex-sync@local}"
if [[ ! -d "$project_dir/.git" ]]; then
write_status "error" "Project is not a git repository"
echo "Project is not a git repository: $project_dir" >"$error_log"
exit 1
fi
if [[ -z "$(git -C "$project_dir" config user.name || true)" ]]; then
git -C "$project_dir" config user.name "$git_name"
fi
if [[ -z "$(git -C "$project_dir" config user.email || true)" ]]; then
git -C "$project_dir" config user.email "$git_email"
fi
if git -C "$project_dir" show-ref --verify --quiet "refs/heads/$branch"; then
git -C "$project_dir" checkout "$branch" >>"$error_log" 2>&1
else
git -C "$project_dir" checkout -B "$branch" >>"$error_log" 2>&1
fi
if [[ -n "$(git -C "$project_dir" status --porcelain)" ]]; then
git -C "$project_dir" add -A >>"$error_log" 2>&1
if ! git -C "$project_dir" diff --cached --quiet; then
stamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
git -C "$project_dir" commit -m "autosync(${host_label}): save work ${stamp}" >>"$error_log" 2>&1 || {
write_status "error" "Failed to commit local changes before sync"
exit 1
}
fi
fi
if ! run_git fetch origin; then
write_status "error" "Failed to fetch origin"
exit 1
fi
if git -C "$project_dir" show-ref --verify --quiet "refs/remotes/origin/$branch"; then
if ! git -C "$project_dir" rebase "origin/$branch" >>"$error_log" 2>&1; then
block_sync "Rebase conflict while replaying local commits onto origin/$branch"
fi
fi
push_ok=0
for attempt in 1 2; do
if git -C "$project_dir" push -u origin "$branch" >>"$error_log" 2>&1; then
push_ok=1
break
fi
if [[ "$attempt" -eq 2 ]]; then
break
fi
if ! run_git fetch origin; then
break
fi
if git -C "$project_dir" show-ref --verify --quiet "refs/remotes/origin/$branch"; then
if ! git -C "$project_dir" rebase "origin/$branch" >>"$error_log" 2>&1; then
block_sync "Push retry failed because origin/$branch caused a rebase conflict"
fi
fi
done
if [[ "$push_ok" -ne 1 ]]; then
write_status "error" "Push failed after retry"
exit 1
fi
rm -f "$blocked_path"
head_commit="$(git -C "$project_dir" rev-parse --short HEAD 2>/dev/null || true)"
write_status "ok" "Synchronized successfully at ${head_commit:-unknown}"