run_update_and_commit.sh 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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.server.json}"
  16. export GIT_TERMINAL_PROMPT=0
  17. commit_name="${GIT_COMMIT_NAME:-vmess-domain-rotator}"
  18. commit_email="${GIT_COMMIT_EMAIL:-vmess-domain-rotator@localhost}"
  19. runtime_branch="${GIT_RUNTIME_BRANCH:-runtime-state}"
  20. push_remote="${GIT_PUSH_REMOTE:-origin}"
  21. push_enabled="${GIT_PUSH_ENABLED:-1}"
  22. push_required="${GIT_PUSH_REQUIRED:-${push_enabled}}"
  23. http_user="${GIT_HTTP_USERNAME:-git}"
  24. http_token="${GIT_HTTP_TOKEN:-}"
  25. http_token_file="${GIT_HTTP_TOKEN_FILE:-}"
  26. credential_helper="${GIT_CREDENTIAL_HELPER:-}"
  27. ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
  28. if [[ -z "$http_token" ]] && [[ -n "$http_token_file" ]] && [[ -r "$http_token_file" ]]; then
  29. http_token="$(tr -d '\r\n' < "$http_token_file")"
  30. fi
  31. git_auth_opts=()
  32. if [[ -n "$http_token" ]]; then
  33. auth_b64="$(printf '%s:%s' "$http_user" "$http_token" | base64 | tr -d '\n')"
  34. git_auth_opts=(-c "http.extraHeader=Authorization: Basic ${auth_b64}")
  35. fi
  36. git_auth() {
  37. local dir="$1"
  38. shift
  39. if [[ -n "$credential_helper" ]]; then
  40. git -C "$dir" -c credential.helper="$credential_helper" "${git_auth_opts[@]}" "$@"
  41. else
  42. git -C "$dir" "${git_auth_opts[@]}" "$@"
  43. fi
  44. }
  45. output_settings_json="$(/usr/bin/python3 "${APP_DIR}/scripts/domain_updater.py" --config "$CONFIG_PATH" --print-output-settings)"
  46. mapfile -t output_settings < <(
  47. printf '%s' "$output_settings_json" | /usr/bin/python3 -c '
  48. import json, sys
  49. settings = json.load(sys.stdin)
  50. for key in ["runtime_dir", "selected_text_path", "selected_json_path", "state_path", "vars_path"]:
  51. print(settings[key])
  52. '
  53. )
  54. RUNTIME_DIR="${output_settings[0]}"
  55. SELECTED_TEXT_FILE="${output_settings[1]}"
  56. SELECTED_JSON_FILE="${output_settings[2]}"
  57. STATE_FILE="${output_settings[3]}"
  58. VARS_FILE="${output_settings[4]}"
  59. repo_relpath() {
  60. /usr/bin/python3 - "$APP_DIR" "$1" <<'PY'
  61. import os
  62. import sys
  63. base = os.path.realpath(sys.argv[1])
  64. path = os.path.realpath(sys.argv[2])
  65. try:
  66. common = os.path.commonpath([base, path])
  67. except ValueError:
  68. common = ""
  69. if common != base:
  70. print("")
  71. else:
  72. print(os.path.relpath(path, base))
  73. PY
  74. }
  75. /usr/bin/python3 "${APP_DIR}/scripts/domain_updater.py" --config "$CONFIG_PATH"
  76. if [[ ! -f "$SELECTED_TEXT_FILE" ]]; then
  77. echo "[vmess-domain-rotator] selected value file missing after updater run (${SELECTED_TEXT_FILE}), skip git commit"
  78. exit 0
  79. fi
  80. after="$(tr -d '\r\n' < "$SELECTED_TEXT_FILE")"
  81. if [[ -z "$after" ]]; then
  82. echo "[vmess-domain-rotator] empty selected value, skip git commit"
  83. exit 0
  84. fi
  85. selected_rel="$(repo_relpath "$SELECTED_TEXT_FILE")"
  86. if [[ -z "$selected_rel" ]]; then
  87. echo "[vmess-domain-rotator] selected value file is outside repo (${SELECTED_TEXT_FILE}), skip git commit"
  88. exit 0
  89. fi
  90. if ! command -v git >/dev/null 2>&1; then
  91. echo "[vmess-domain-rotator] git not found, skip git commit"
  92. exit 0
  93. fi
  94. if ! git -C "$APP_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  95. echo "[vmess-domain-rotator] not a git repo at ${APP_DIR}, skip git commit"
  96. echo "[vmess-domain-rotator] hint: reinstall service from a git clone path"
  97. exit 0
  98. fi
  99. if ! git -C "$APP_DIR" rev-parse --verify HEAD >/dev/null 2>&1; then
  100. echo "[vmess-domain-rotator] repo has no commits yet, skip git commit"
  101. exit 0
  102. fi
  103. if ! git -C "$APP_DIR" remote get-url "$push_remote" >/dev/null 2>&1; then
  104. push_remote=""
  105. while IFS= read -r r; do
  106. push_remote="$r"
  107. break
  108. done < <(git -C "$APP_DIR" remote)
  109. fi
  110. work_dir=""
  111. cleanup_worktree="0"
  112. current_branch="$(git -C "$APP_DIR" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
  113. if [[ "$current_branch" == "$runtime_branch" ]]; then
  114. work_dir="$APP_DIR"
  115. else
  116. work_dir="$(mktemp -d "${TMPDIR:-/tmp}/vmess-runtime-state.XXXXXX")"
  117. cleanup_worktree="1"
  118. if git -C "$APP_DIR" show-ref --verify --quiet "refs/heads/${runtime_branch}"; then
  119. git -C "$APP_DIR" worktree add --force "$work_dir" "$runtime_branch"
  120. else
  121. if [[ -n "$push_remote" ]] && git_auth "$APP_DIR" ls-remote --exit-code --heads "$push_remote" "$runtime_branch" >/dev/null 2>&1; then
  122. git_auth "$APP_DIR" fetch "$push_remote" "$runtime_branch:$runtime_branch"
  123. git -C "$APP_DIR" worktree add --force "$work_dir" "$runtime_branch"
  124. else
  125. git -C "$APP_DIR" worktree add --force --detach "$work_dir"
  126. git -C "$work_dir" checkout --orphan "$runtime_branch"
  127. git -C "$work_dir" rm -rf . >/dev/null 2>&1 || true
  128. echo "[vmess-domain-rotator] initialized branch ${runtime_branch}"
  129. fi
  130. fi
  131. fi
  132. if [[ "$cleanup_worktree" == "1" ]]; then
  133. cleanup() {
  134. git -C "$APP_DIR" worktree remove --force "$work_dir" >/dev/null 2>&1 || rm -rf "$work_dir"
  135. }
  136. trap cleanup EXIT
  137. fi
  138. work_branch="$(git -C "$work_dir" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
  139. if [[ "$work_branch" != "$runtime_branch" ]]; then
  140. echo "[vmess-domain-rotator] safety check failed: worktree branch is '${work_branch:-detached}', expected '${runtime_branch}'"
  141. exit 1
  142. fi
  143. before=""
  144. if before_raw="$(git -C "$work_dir" show "HEAD:${selected_rel}" 2>/dev/null)"; then
  145. before="$(printf '%s' "$before_raw" | tr -d '\r\n')"
  146. fi
  147. if [[ "$force_commit" != "1" ]] && [[ -n "$before" ]] && [[ "$after" == "$before" ]]; then
  148. echo "[vmess-domain-rotator] selected value unchanged (${after}), skip git commit and push"
  149. exit 0
  150. fi
  151. tracked_src_files=("$SELECTED_TEXT_FILE" "$SELECTED_JSON_FILE" "$STATE_FILE" "$VARS_FILE")
  152. tracked_rel_files=()
  153. for src in "${tracked_src_files[@]}"; do
  154. rel="$(repo_relpath "$src")"
  155. if [[ -z "$rel" ]]; then
  156. echo "[vmess-domain-rotator] skip non-repo output file: ${src}"
  157. continue
  158. fi
  159. tracked_rel_files+=("$rel")
  160. dst="$work_dir/$rel"
  161. mkdir -p "$(dirname "$dst")"
  162. if [[ -f "$src" ]]; then
  163. cp "$src" "$dst"
  164. else
  165. rm -f "$dst"
  166. fi
  167. done
  168. if [[ "${#tracked_rel_files[@]}" -eq 0 ]]; then
  169. echo "[vmess-domain-rotator] no repo-local output files from config (${CONFIG_PATH}), skip git commit"
  170. exit 0
  171. fi
  172. git -C "$work_dir" add -A -- "${tracked_rel_files[@]}" || true
  173. staged_changed="1"
  174. if git -C "$work_dir" diff --cached --quiet; then
  175. staged_changed="0"
  176. fi
  177. if [[ "$staged_changed" == "0" ]] && [[ "$force_commit" != "1" ]]; then
  178. echo "[vmess-domain-rotator] no staged changes for ${runtime_branch}, skip git commit"
  179. exit 0
  180. fi
  181. commit_extra_args=()
  182. if [[ "$staged_changed" == "0" ]] && [[ "$force_commit" == "1" ]]; then
  183. commit_extra_args+=(--allow-empty)
  184. echo "[vmess-domain-rotator] force commit enabled with unchanged content, creating empty commit"
  185. fi
  186. commit_message="chore: rotate preferred value to ${after} (${ts})"
  187. if [[ "$force_commit" == "1" ]]; then
  188. commit_message="manual: value ${after}, updated at ${ts}"
  189. fi
  190. git -C "$work_dir" \
  191. -c user.name="$commit_name" \
  192. -c user.email="$commit_email" \
  193. commit "${commit_extra_args[@]}" -m "$commit_message"
  194. if [[ "$push_enabled" != "1" ]]; then
  195. echo "[vmess-domain-rotator] git push disabled by GIT_PUSH_ENABLED=${push_enabled}"
  196. elif [[ -z "$push_remote" ]]; then
  197. if [[ "$push_required" == "1" ]]; then
  198. echo "[vmess-domain-rotator] no remote found but push is required"
  199. exit 1
  200. fi
  201. echo "[vmess-domain-rotator] no remote found, skip git push"
  202. else
  203. push_ok="0"
  204. if git -C "$work_dir" rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
  205. if git_auth "$work_dir" push "$push_remote" "$runtime_branch:$runtime_branch"; then
  206. push_ok="1"
  207. echo "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch}"
  208. fi
  209. else
  210. if git_auth "$work_dir" push -u "$push_remote" "$runtime_branch:$runtime_branch"; then
  211. push_ok="1"
  212. echo "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch} (set upstream)"
  213. fi
  214. fi
  215. if [[ "$push_ok" != "1" ]]; then
  216. echo "[vmess-domain-rotator] git push failed"
  217. echo "[vmess-domain-rotator] hint: configure non-interactive auth (credential.helper store, SSH deploy key, or GIT_HTTP_USERNAME/GIT_HTTP_TOKEN)"
  218. if [[ "$push_required" == "1" ]]; then
  219. exit 1
  220. fi
  221. fi
  222. fi
  223. echo "[vmess-domain-rotator] committed output changes on ${runtime_branch}: selected value ${after} from ${RUNTIME_DIR}"