run_update_and_commit.sh 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  4. DEFAULT_APP_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
  5. APP_DIR="$(git -C "$DEFAULT_APP_DIR" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$DEFAULT_APP_DIR")"
  6. force_commit="${GIT_FORCE_COMMIT:-0}"
  7. if [[ "${1:-}" == "--force-commit" ]]; then
  8. force_commit="1"
  9. shift
  10. fi
  11. if [[ ! "$force_commit" =~ ^[01]$ ]]; then
  12. echo "[vmess-domain-rotator] invalid GIT_FORCE_COMMIT=${force_commit}, expected 0 or 1"
  13. exit 1
  14. fi
  15. CONFIG_PATH="${1:-${APP_DIR}/config.json}"
  16. DOMAIN_FILE="${APP_DIR}/runtime/current_domain.txt"
  17. export GIT_TERMINAL_PROMPT=0
  18. commit_name="${GIT_COMMIT_NAME:-vmess-domain-rotator}"
  19. commit_email="${GIT_COMMIT_EMAIL:-vmess-domain-rotator@localhost}"
  20. runtime_branch="${GIT_RUNTIME_BRANCH:-runtime-state}"
  21. push_remote="${GIT_PUSH_REMOTE:-origin}"
  22. push_enabled="${GIT_PUSH_ENABLED:-1}"
  23. push_required="${GIT_PUSH_REQUIRED:-${push_enabled}}"
  24. http_user="${GIT_HTTP_USERNAME:-git}"
  25. http_token="${GIT_HTTP_TOKEN:-}"
  26. http_token_file="${GIT_HTTP_TOKEN_FILE:-}"
  27. credential_helper="${GIT_CREDENTIAL_HELPER:-}"
  28. ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
  29. if [[ -z "$http_token" ]] && [[ -n "$http_token_file" ]] && [[ -r "$http_token_file" ]]; then
  30. http_token="$(tr -d '\r\n' < "$http_token_file")"
  31. fi
  32. git_auth_opts=()
  33. if [[ -n "$http_token" ]]; then
  34. auth_b64="$(printf '%s:%s' "$http_user" "$http_token" | base64 | tr -d '\n')"
  35. git_auth_opts=(-c "http.extraHeader=Authorization: Basic ${auth_b64}")
  36. fi
  37. git_auth() {
  38. local dir="$1"
  39. shift
  40. if [[ -n "$credential_helper" ]]; then
  41. git -C "$dir" -c credential.helper="$credential_helper" "${git_auth_opts[@]}" "$@"
  42. else
  43. git -C "$dir" "${git_auth_opts[@]}" "$@"
  44. fi
  45. }
  46. /usr/bin/python3 "${APP_DIR}/scripts/domain_updater.py" --config "$CONFIG_PATH"
  47. if [[ ! -f "$DOMAIN_FILE" ]]; then
  48. echo "[vmess-domain-rotator] runtime/current_domain.txt missing after updater run, skip git commit"
  49. exit 0
  50. fi
  51. after="$(tr -d '\r\n' < "$DOMAIN_FILE")"
  52. if [[ -z "$after" ]]; then
  53. echo "[vmess-domain-rotator] empty selected domain, skip git commit"
  54. exit 0
  55. fi
  56. if ! command -v git >/dev/null 2>&1; then
  57. echo "[vmess-domain-rotator] git not found, skip git commit"
  58. exit 0
  59. fi
  60. if ! git -C "$APP_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  61. echo "[vmess-domain-rotator] not a git repo at ${APP_DIR}, skip git commit"
  62. echo "[vmess-domain-rotator] hint: reinstall service from a git clone path"
  63. exit 0
  64. fi
  65. if ! git -C "$APP_DIR" rev-parse --verify HEAD >/dev/null 2>&1; then
  66. echo "[vmess-domain-rotator] repo has no commits yet, skip git commit"
  67. exit 0
  68. fi
  69. if ! git -C "$APP_DIR" remote get-url "$push_remote" >/dev/null 2>&1; then
  70. push_remote=""
  71. while IFS= read -r r; do
  72. push_remote="$r"
  73. break
  74. done < <(git -C "$APP_DIR" remote)
  75. fi
  76. work_dir=""
  77. cleanup_worktree="0"
  78. current_branch="$(git -C "$APP_DIR" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
  79. if [[ "$current_branch" == "$runtime_branch" ]]; then
  80. work_dir="$APP_DIR"
  81. else
  82. work_dir="$(mktemp -d "${TMPDIR:-/tmp}/vmess-runtime-state.XXXXXX")"
  83. cleanup_worktree="1"
  84. if git -C "$APP_DIR" show-ref --verify --quiet "refs/heads/${runtime_branch}"; then
  85. git -C "$APP_DIR" worktree add --force "$work_dir" "$runtime_branch"
  86. else
  87. if [[ -n "$push_remote" ]] && git_auth "$APP_DIR" ls-remote --exit-code --heads "$push_remote" "$runtime_branch" >/dev/null 2>&1; then
  88. git_auth "$APP_DIR" fetch "$push_remote" "$runtime_branch:$runtime_branch"
  89. git -C "$APP_DIR" worktree add --force "$work_dir" "$runtime_branch"
  90. else
  91. git -C "$APP_DIR" worktree add --force --detach "$work_dir"
  92. git -C "$work_dir" checkout --orphan "$runtime_branch"
  93. git -C "$work_dir" rm -rf . >/dev/null 2>&1 || true
  94. echo "[vmess-domain-rotator] initialized branch ${runtime_branch}"
  95. fi
  96. fi
  97. fi
  98. if [[ "$cleanup_worktree" == "1" ]]; then
  99. cleanup() {
  100. git -C "$APP_DIR" worktree remove --force "$work_dir" >/dev/null 2>&1 || rm -rf "$work_dir"
  101. }
  102. trap cleanup EXIT
  103. fi
  104. work_branch="$(git -C "$work_dir" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
  105. if [[ "$work_branch" != "$runtime_branch" ]]; then
  106. echo "[vmess-domain-rotator] safety check failed: worktree branch is '${work_branch:-detached}', expected '${runtime_branch}'"
  107. exit 1
  108. fi
  109. before=""
  110. if before_raw="$(git -C "$work_dir" show "HEAD:runtime/current_domain.txt" 2>/dev/null)"; then
  111. before="$(printf '%s' "$before_raw" | tr -d '\r\n')"
  112. fi
  113. if [[ "$force_commit" != "1" ]] && [[ -n "$before" ]] && [[ "$after" == "$before" ]]; then
  114. echo "[vmess-domain-rotator] selected domain unchanged (${after}), skip git commit and push"
  115. exit 0
  116. fi
  117. mkdir -p "$work_dir/runtime"
  118. for file in current_domain.txt current_domain.json state.json substore_vars.json; do
  119. src="$APP_DIR/runtime/$file"
  120. dst="$work_dir/runtime/$file"
  121. if [[ -f "$src" ]]; then
  122. cp "$src" "$dst"
  123. else
  124. rm -f "$dst"
  125. fi
  126. done
  127. git -C "$work_dir" add -A runtime/current_domain.txt runtime/current_domain.json runtime/state.json runtime/substore_vars.json || true
  128. staged_changed="1"
  129. if git -C "$work_dir" diff --cached --quiet; then
  130. staged_changed="0"
  131. fi
  132. if [[ "$staged_changed" == "0" ]] && [[ "$force_commit" != "1" ]]; then
  133. echo "[vmess-domain-rotator] no staged changes for ${runtime_branch}, skip git commit"
  134. exit 0
  135. fi
  136. commit_extra_args=()
  137. if [[ "$staged_changed" == "0" ]] && [[ "$force_commit" == "1" ]]; then
  138. commit_extra_args+=(--allow-empty)
  139. echo "[vmess-domain-rotator] force commit enabled with unchanged content, creating empty commit"
  140. fi
  141. commit_message="chore: rotate preferred domain to ${after} (${ts})"
  142. if [[ "$force_commit" == "1" ]]; then
  143. commit_message="manual: domain ${after}, updated at ${ts}"
  144. fi
  145. git -C "$work_dir" \
  146. -c user.name="$commit_name" \
  147. -c user.email="$commit_email" \
  148. commit "${commit_extra_args[@]}" -m "$commit_message"
  149. if [[ "$push_enabled" != "1" ]]; then
  150. echo "[vmess-domain-rotator] git push disabled by GIT_PUSH_ENABLED=${push_enabled}"
  151. elif [[ -z "$push_remote" ]]; then
  152. if [[ "$push_required" == "1" ]]; then
  153. echo "[vmess-domain-rotator] no remote found but push is required"
  154. exit 1
  155. fi
  156. echo "[vmess-domain-rotator] no remote found, skip git push"
  157. else
  158. push_ok="0"
  159. if git -C "$work_dir" rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
  160. if git_auth "$work_dir" push "$push_remote" "$runtime_branch:$runtime_branch"; then
  161. push_ok="1"
  162. echo "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch}"
  163. fi
  164. else
  165. if git_auth "$work_dir" push -u "$push_remote" "$runtime_branch:$runtime_branch"; then
  166. push_ok="1"
  167. echo "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch} (set upstream)"
  168. fi
  169. fi
  170. if [[ "$push_ok" != "1" ]]; then
  171. echo "[vmess-domain-rotator] git push failed"
  172. echo "[vmess-domain-rotator] hint: configure non-interactive auth (credential.helper store, SSH deploy key, or GIT_HTTP_USERNAME/GIT_HTTP_TOKEN)"
  173. if [[ "$push_required" == "1" ]]; then
  174. exit 1
  175. fi
  176. fi
  177. fi
  178. echo "[vmess-domain-rotator] committed runtime changes on ${runtime_branch}: selected domain ${after}"