run_update_and_commit.sh 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. # --- journald tee: log to journal even when run manually ---
  4. if [[ -z "${INVOCATION_ID:-}" ]] && command -v systemd-cat >/dev/null 2>&1; then
  5. exec > >(tee >(systemd-cat -t vmess-domain-rotator -p info)) \
  6. 2> >(tee >(systemd-cat -t vmess-domain-rotator -p err) >&2)
  7. fi
  8. timestamp() {
  9. date '+%Y-%m-%d %H:%M:%S'
  10. }
  11. log() {
  12. printf '[%s] %s\n' "$(timestamp)" "$*"
  13. }
  14. log_err() {
  15. printf '[%s] %s\n' "$(timestamp)" "$*" >&2
  16. }
  17. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  18. DEFAULT_APP_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
  19. APP_DIR="${APP_DIR:-$(git -C "$DEFAULT_APP_DIR" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$DEFAULT_APP_DIR")}"
  20. PYTHON_BIN="${PYTHON_BIN:-}"
  21. if [[ -z "$PYTHON_BIN" ]]; then
  22. if [[ -x "/usr/bin/python3" ]]; then
  23. PYTHON_BIN="/usr/bin/python3"
  24. else
  25. PYTHON_BIN="$(command -v python3 || echo "python3")"
  26. fi
  27. fi
  28. force_commit="${GIT_FORCE_COMMIT:-0}"
  29. if [[ "${1:-}" == "--force-commit" ]]; then
  30. force_commit="1"
  31. shift
  32. fi
  33. if [[ ! "$force_commit" =~ ^[01]$ ]]; then
  34. log "[vmess-domain-rotator] invalid GIT_FORCE_COMMIT=${force_commit}, expected 0 or 1"
  35. exit 1
  36. fi
  37. if [[ -z "${1:-}" ]]; then
  38. if [[ -r "${APP_DIR}/configs/config.server.json" ]]; then
  39. CONFIG_PATH="${APP_DIR}/configs/config.server.json"
  40. else
  41. CONFIG_PATH="${APP_DIR}/config.server.json"
  42. fi
  43. else
  44. CONFIG_PATH="$1"
  45. if [[ ! -r "$CONFIG_PATH" ]] && [[ -r "${APP_DIR}/configs/${CONFIG_PATH}" ]]; then
  46. CONFIG_PATH="${APP_DIR}/configs/${CONFIG_PATH}"
  47. elif [[ ! -r "$CONFIG_PATH" ]] && [[ -r "${APP_DIR}/configs/$(basename "$CONFIG_PATH")" ]]; then
  48. CONFIG_PATH="${APP_DIR}/configs/$(basename "$CONFIG_PATH")"
  49. fi
  50. fi
  51. # --- adaptive interval check ---
  52. if [[ "$force_commit" != "1" ]]; then
  53. peak_start=$((10#${PEAK_START_HOUR:-19}))
  54. peak_end=$((10#${PEAK_END_HOUR:-24}))
  55. peak_tz="${PEAK_TZ:-Asia/Shanghai}"
  56. peak_int=$((10#${PEAK_INTERVAL_MIN:-10}))
  57. offpeak_int=$((10#${OFFPEAK_INTERVAL_MIN:-30}))
  58. # Get current hour and minute in peak timezone
  59. curr_hour=$((10#$(TZ="$peak_tz" date '+%H')))
  60. curr_min=$((10#$(TZ="$peak_tz" date '+%M')))
  61. in_peak=0
  62. if [[ "$peak_start" -eq "$peak_end" ]]; then
  63. # start == end: no peak window defined, always off-peak
  64. in_peak=0
  65. elif [[ "$peak_start" -lt "$peak_end" ]]; then
  66. # Normal range (e.g. 8 to 22)
  67. if [[ "$curr_hour" -ge "$peak_start" ]] && [[ "$curr_hour" -lt "$peak_end" ]]; then
  68. in_peak=1
  69. fi
  70. else
  71. # Crossing midnight (e.g. peak-start 22, peak-end 2)
  72. if [[ "$curr_hour" -ge "$peak_start" ]] || [[ "$curr_hour" -lt "$peak_end" ]]; then
  73. in_peak=1
  74. fi
  75. fi
  76. if [[ "$in_peak" -eq 0 ]]; then
  77. # Outside peak hours: only execute near offpeak_int minute boundaries (e.g. 0, 30)
  78. # Allow drift tolerance of half the timer tick (peak_int / 2)
  79. remainder=$((curr_min % offpeak_int))
  80. tolerance=$(( (peak_int + 1) / 2 ))
  81. if [[ $remainder -gt $tolerance ]] && [[ $remainder -lt $((offpeak_int - tolerance)) ]]; then
  82. log "[vmess-domain-rotator] off-peak run skipped: hour=${curr_hour} minute=${curr_min} (remainder=${remainder}m, next at ${offpeak_int}m boundary)"
  83. exit 0
  84. fi
  85. fi
  86. # During peak hours: always execute (timer already fires at peak_int interval)
  87. fi
  88. export GIT_TERMINAL_PROMPT=0
  89. commit_name="${GIT_COMMIT_NAME:-vmess-domain-rotator}"
  90. commit_email="${GIT_COMMIT_EMAIL:-vmess-domain-rotator@localhost}"
  91. runtime_branch="${GIT_RUNTIME_BRANCH:-runtime-state}"
  92. push_remote="${GIT_PUSH_REMOTE:-origin}"
  93. push_enabled="${GIT_PUSH_ENABLED:-1}"
  94. push_required="${GIT_PUSH_REQUIRED:-${push_enabled}}"
  95. http_user="${GIT_HTTP_USERNAME:-git}"
  96. http_token="${GIT_HTTP_TOKEN:-}"
  97. http_token_file="${GIT_HTTP_TOKEN_FILE:-}"
  98. credential_helper="${GIT_CREDENTIAL_HELPER:-}"
  99. max_state_commits="${GIT_MAX_STATE_COMMITS:-50}"
  100. ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
  101. if [[ -z "$http_token" ]] && [[ -n "$http_token_file" ]] && [[ -r "$http_token_file" ]]; then
  102. http_token="$(tr -d '\r\n' < "$http_token_file")"
  103. fi
  104. git_auth() {
  105. local dir="$1"
  106. shift
  107. local -a env_vars=()
  108. if [[ -n "$http_token" ]]; then
  109. local auth_b64
  110. auth_b64="$(printf '%s:%s' "$http_user" "$http_token" | base64 | tr -d '\n')"
  111. env_vars+=(
  112. "GIT_CONFIG_COUNT=1"
  113. "GIT_CONFIG_KEY_0=http.extraHeader"
  114. "GIT_CONFIG_VALUE_0=Authorization: Basic ${auth_b64}"
  115. )
  116. fi
  117. if [[ -n "$credential_helper" ]]; then
  118. env "${env_vars[@]}" git -C "$dir" -c credential.helper="$credential_helper" "$@"
  119. else
  120. env "${env_vars[@]}" git -C "$dir" "$@"
  121. fi
  122. }
  123. output_settings_json="$("$PYTHON_BIN" "${APP_DIR}/scripts/domain_updater.py" --config "$CONFIG_PATH" --print-output-settings)"
  124. output_settings=()
  125. while IFS= read -r line; do
  126. output_settings+=("$line")
  127. done < <(
  128. printf '%s' "$output_settings_json" | "$PYTHON_BIN" -c '
  129. import json, sys
  130. settings = json.load(sys.stdin)
  131. for key in ["runtime_dir", "selected_text_path", "selected_json_path", "state_path", "vars_path"]:
  132. print(settings[key])
  133. '
  134. )
  135. RUNTIME_DIR="${output_settings[0]}"
  136. SELECTED_TEXT_FILE="${output_settings[1]}"
  137. SELECTED_JSON_FILE="${output_settings[2]}"
  138. STATE_FILE="${output_settings[3]}"
  139. VARS_FILE="${output_settings[4]}"
  140. repo_relpath() {
  141. "$PYTHON_BIN" - "$APP_DIR" "$1" <<'PY'
  142. import os
  143. import sys
  144. base = os.path.realpath(sys.argv[1])
  145. path = os.path.realpath(sys.argv[2])
  146. try:
  147. common = os.path.commonpath([base, path])
  148. except ValueError:
  149. common = ""
  150. if common != base:
  151. print("")
  152. else:
  153. print(os.path.relpath(path, base))
  154. PY
  155. }
  156. json_field() {
  157. local key="$1"
  158. "$PYTHON_BIN" -c '
  159. import json
  160. import sys
  161. key = sys.argv[1]
  162. try:
  163. obj = json.load(sys.stdin)
  164. except Exception:
  165. print("")
  166. else:
  167. value = obj.get(key, "")
  168. print("" if value is None else str(value))
  169. ' "$key"
  170. }
  171. tmp_result_json="$(mktemp "${TMPDIR:-/tmp}/vmess_result.XXXXXX")"
  172. trap 'rm -f "$tmp_result_json"' EXIT INT TERM
  173. "$PYTHON_BIN" "${APP_DIR}/scripts/domain_updater.py" --config "$CONFIG_PATH" --result-json-file "$tmp_result_json"
  174. if [[ -s "$tmp_result_json" ]]; then
  175. updater_status="$(json_field status < "$tmp_result_json" 2>/dev/null || echo "")"
  176. else
  177. updater_status=""
  178. fi
  179. rm -f "$tmp_result_json"
  180. if [[ "$updater_status" == "error_use_last_good" || "$updater_status" == "fallback_last_good" ]]; then
  181. log "[vmess-domain-rotator] warning: updater using fallback value (${updater_status})"
  182. fi
  183. if [[ ! -f "$SELECTED_TEXT_FILE" ]]; then
  184. log "[vmess-domain-rotator] selected value file missing after updater run (${SELECTED_TEXT_FILE}), skip git commit"
  185. exit 0
  186. fi
  187. after="$(tr -d '\r\n' < "$SELECTED_TEXT_FILE")"
  188. if [[ -z "$after" ]]; then
  189. log "[vmess-domain-rotator] empty selected value, skip git commit"
  190. exit 0
  191. fi
  192. selected_rel="$(repo_relpath "$SELECTED_TEXT_FILE")"
  193. selected_json_rel="$(repo_relpath "$SELECTED_JSON_FILE")"
  194. vars_rel="$(repo_relpath "$VARS_FILE")"
  195. if [[ -z "$selected_rel" ]]; then
  196. log "[vmess-domain-rotator] selected value file is outside repo (${SELECTED_TEXT_FILE}), skip git commit"
  197. exit 0
  198. fi
  199. if ! command -v git >/dev/null 2>&1; then
  200. log "[vmess-domain-rotator] git not found, skip git commit"
  201. exit 0
  202. fi
  203. if ! git -C "$APP_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  204. log "[vmess-domain-rotator] not a git repo at ${APP_DIR}, skip git commit"
  205. log "[vmess-domain-rotator] hint: reinstall service from a git clone path"
  206. exit 0
  207. fi
  208. if ! git -C "$APP_DIR" rev-parse --verify HEAD >/dev/null 2>&1; then
  209. log "[vmess-domain-rotator] repo has no commits yet, skip git commit"
  210. exit 0
  211. fi
  212. if ! git -C "$APP_DIR" remote get-url "$push_remote" >/dev/null 2>&1; then
  213. push_remote=""
  214. while IFS= read -r r; do
  215. push_remote="$r"
  216. break
  217. done < <(git -C "$APP_DIR" remote)
  218. fi
  219. work_dir=""
  220. cleanup_worktree="0"
  221. current_branch="$(git -C "$APP_DIR" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
  222. if [[ "$current_branch" == "$runtime_branch" ]]; then
  223. work_dir="$APP_DIR"
  224. else
  225. work_dir="$(mktemp -d "${TMPDIR:-/tmp}/vmess-runtime-state.XXXXXX")"
  226. cleanup_worktree="1"
  227. if git -C "$APP_DIR" show-ref --verify --quiet "refs/heads/${runtime_branch}"; then
  228. git -C "$APP_DIR" worktree add --force "$work_dir" "$runtime_branch"
  229. else
  230. remote_branch_exists="0"
  231. if [[ -n "$push_remote" ]]; then
  232. set +e
  233. git_auth "$APP_DIR" ls-remote --exit-code --heads "$push_remote" "$runtime_branch" >/dev/null 2>&1
  234. ls_remote_rc=$?
  235. set -e
  236. if [[ $ls_remote_rc -eq 0 ]]; then
  237. remote_branch_exists="1"
  238. elif [[ $ls_remote_rc -eq 2 ]]; then
  239. remote_branch_exists="0"
  240. else
  241. log_err "[vmess-domain-rotator] fatal: git ls-remote failed with exit code ${ls_remote_rc} on remote ${push_remote}"
  242. exit 1
  243. fi
  244. fi
  245. if [[ "$remote_branch_exists" == "1" ]]; then
  246. git_auth "$APP_DIR" fetch "$push_remote" "$runtime_branch:$runtime_branch"
  247. git -C "$APP_DIR" worktree add --force "$work_dir" "$runtime_branch"
  248. else
  249. git -C "$APP_DIR" worktree add --force --detach "$work_dir"
  250. git -C "$work_dir" checkout --orphan "$runtime_branch"
  251. git -C "$work_dir" rm -rf . >/dev/null 2>&1 || true
  252. log "[vmess-domain-rotator] initialized branch ${runtime_branch}"
  253. fi
  254. fi
  255. fi
  256. if [[ "$cleanup_worktree" == "1" ]]; then
  257. cleanup() {
  258. trap - EXIT INT TERM
  259. git -C "$APP_DIR" worktree remove --force "$work_dir" >/dev/null 2>&1 || {
  260. rm -rf "$work_dir"
  261. git -C "$APP_DIR" worktree prune >/dev/null 2>&1 || true
  262. }
  263. }
  264. trap cleanup EXIT
  265. trap 'cleanup; exit 1' INT TERM
  266. fi
  267. work_branch="$(git -C "$work_dir" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
  268. if [[ "$work_branch" != "$runtime_branch" ]]; then
  269. log "[vmess-domain-rotator] safety check failed: worktree branch is '${work_branch:-detached}', expected '${runtime_branch}'"
  270. exit 1
  271. fi
  272. before=""
  273. if before_raw="$(git -C "$work_dir" show "HEAD:${selected_rel}" 2>/dev/null)"; then
  274. before="$(printf '%s' "$before_raw" | tr -d '\r\n')"
  275. fi
  276. before_status=""
  277. if [[ -n "$selected_json_rel" ]] && before_json_raw="$(git -C "$work_dir" show "HEAD:${selected_json_rel}" 2>/dev/null)"; then
  278. before_status="$(printf '%s' "$before_json_raw" | json_field status 2>/dev/null || echo "")"
  279. fi
  280. before_vars_status=""
  281. if [[ -n "$vars_rel" ]] && before_vars_raw="$(git -C "$work_dir" show "HEAD:${vars_rel}" 2>/dev/null)"; then
  282. before_vars_status="$(printf '%s' "$before_vars_raw" | json_field STATUS 2>/dev/null || echo "")"
  283. fi
  284. status_compared="0"
  285. status_unchanged="0"
  286. if [[ -n "$updater_status" ]]; then
  287. status_unchanged="1"
  288. if [[ -n "$before_status" ]]; then
  289. status_compared="1"
  290. [[ "$updater_status" != "$before_status" ]] && status_unchanged="0"
  291. fi
  292. if [[ -n "$before_vars_status" ]]; then
  293. status_compared="1"
  294. [[ "$updater_status" != "$before_vars_status" ]] && status_unchanged="0"
  295. fi
  296. if [[ "$status_compared" != "1" ]]; then
  297. status_unchanged="0"
  298. fi
  299. fi
  300. is_fallback="0"
  301. if [[ "$updater_status" == "error_use_last_good" || "$updater_status" == "fallback_last_good" ]]; then
  302. is_fallback="1"
  303. fi
  304. if [[ "$force_commit" != "1" ]] && [[ -n "$before" ]] && [[ "$after" == "$before" ]]; then
  305. if [[ "$status_unchanged" == "1" || "$is_fallback" == "1" ]]; then
  306. log "[vmess-domain-rotator] selected value unchanged (${after}) [status=${updater_status:-unknown}], skip git commit and push"
  307. exit 0
  308. fi
  309. fi
  310. tracked_src_files=("$SELECTED_TEXT_FILE" "$SELECTED_JSON_FILE" "$STATE_FILE" "$VARS_FILE")
  311. tracked_rel_files=()
  312. for src in "${tracked_src_files[@]}"; do
  313. rel="$(repo_relpath "$src")"
  314. if [[ -z "$rel" ]]; then
  315. log "[vmess-domain-rotator] skip non-repo output file: ${src}"
  316. continue
  317. fi
  318. tracked_rel_files+=("$rel")
  319. dst="$work_dir/$rel"
  320. mkdir -p "$(dirname "$dst")"
  321. if [[ -f "$src" ]]; then
  322. if [[ "$src" != "$dst" ]]; then
  323. cp "$src" "$dst"
  324. fi
  325. else
  326. rm -f "$dst"
  327. fi
  328. done
  329. if [[ "${#tracked_rel_files[@]}" -eq 0 ]]; then
  330. log "[vmess-domain-rotator] no repo-local output files from config (${CONFIG_PATH}), skip git commit"
  331. exit 0
  332. fi
  333. git -C "$work_dir" add -A -- "${tracked_rel_files[@]}" || true
  334. staged_changed="1"
  335. if git -C "$work_dir" diff --cached --quiet; then
  336. staged_changed="0"
  337. fi
  338. if [[ "$staged_changed" == "0" ]] && [[ "$force_commit" != "1" ]]; then
  339. log "[vmess-domain-rotator] no staged changes for ${runtime_branch}, skip git commit"
  340. exit 0
  341. fi
  342. commit_extra_args=()
  343. if [[ "$staged_changed" == "0" ]] && [[ "$force_commit" == "1" ]]; then
  344. commit_extra_args+=(--allow-empty)
  345. log "[vmess-domain-rotator] force commit enabled with unchanged content, creating empty commit"
  346. fi
  347. commit_message="chore: rotate preferred value to ${after} (${ts})"
  348. if [[ "$updater_status" == "error_use_last_good" || "$updater_status" == "fallback_last_good" ]]; then
  349. commit_message="chore(fallback): rotate preferred value to ${after} (${ts})"
  350. fi
  351. if [[ "$force_commit" == "1" ]]; then
  352. commit_message="manual: value ${after}, updated at ${ts}"
  353. fi
  354. commit_cmd=(git -C "$work_dir" -c user.name="$commit_name" -c user.email="$commit_email" commit)
  355. if [[ "${#commit_extra_args[@]}" -gt 0 ]]; then
  356. commit_cmd+=("${commit_extra_args[@]}")
  357. fi
  358. commit_cmd+=(-m "$commit_message")
  359. "${commit_cmd[@]}"
  360. history_trimmed="0"
  361. if [[ "$max_state_commits" -gt 0 ]]; then
  362. commit_count="$(git -C "$work_dir" rev-list --count HEAD 2>/dev/null || echo "1")"
  363. if [[ "$commit_count" -gt "$max_state_commits" ]]; then
  364. log "[vmess-domain-rotator] runtime history reached ${commit_count} commits, trimming older history (retaining last ${max_state_commits})"
  365. git -C "$work_dir" branch -f "${runtime_branch}-untrimmed" HEAD >/dev/null 2>&1 || true
  366. base_commit="$(git -C "$work_dir" rev-parse "HEAD~$((max_state_commits - 1))" 2>/dev/null || true)"
  367. if [[ -n "$base_commit" ]]; then
  368. tree_sha="$(git -C "$work_dir" rev-parse "${base_commit}^{tree}" 2>/dev/null || true)"
  369. if [[ -n "$tree_sha" ]]; then
  370. root_commit="$(git -C "$work_dir" commit-tree "$tree_sha" -m "chore(prune): baseline snapshot before rolling rotation" 2>/dev/null || true)"
  371. if [[ -n "$root_commit" ]] && git -C "$work_dir" rebase --onto "$root_commit" "$base_commit" "$runtime_branch" >/dev/null 2>&1; then
  372. history_trimmed="1"
  373. log "[vmess-domain-rotator] history successfully pruned to ${max_state_commits} commits"
  374. else
  375. git -C "$work_dir" reset --hard "${runtime_branch}-untrimmed" >/dev/null 2>&1 || true
  376. fi
  377. fi
  378. fi
  379. fi
  380. fi
  381. if [[ "$push_enabled" != "1" ]]; then
  382. log "[vmess-domain-rotator] git push disabled by GIT_PUSH_ENABLED=${push_enabled}"
  383. elif [[ -z "$push_remote" ]]; then
  384. if [[ "$push_required" == "1" ]]; then
  385. log "[vmess-domain-rotator] no remote found but push is required"
  386. exit 1
  387. fi
  388. log "[vmess-domain-rotator] no remote found, skip git push"
  389. else
  390. push_opts=()
  391. if [[ "$history_trimmed" == "1" ]]; then
  392. push_opts+=(--force-with-lease)
  393. fi
  394. push_opts+=("$push_remote" "$runtime_branch:$runtime_branch")
  395. push_ok="0"
  396. if git -C "$work_dir" rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
  397. if git_auth "$work_dir" push "${push_opts[@]}"; then
  398. push_ok="1"
  399. log "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch}"
  400. elif [[ "$history_trimmed" == "1" ]]; then
  401. log "[vmess-domain-rotator] remote rejected force-with-lease for history trimming, restoring full history and retrying normal push"
  402. git -C "$work_dir" reset --hard "${runtime_branch}-untrimmed" >/dev/null 2>&1 || true
  403. if git_auth "$work_dir" push "$push_remote" "$runtime_branch:$runtime_branch"; then
  404. push_ok="1"
  405. log "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch} (fallback to append-only)"
  406. fi
  407. fi
  408. else
  409. if git_auth "$work_dir" push -u "${push_opts[@]}"; then
  410. push_ok="1"
  411. log "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch} (set upstream)"
  412. elif [[ "$history_trimmed" == "1" ]]; then
  413. log "[vmess-domain-rotator] remote rejected force-with-lease for history trimming, restoring full history and retrying normal push"
  414. git -C "$work_dir" reset --hard "${runtime_branch}-untrimmed" >/dev/null 2>&1 || true
  415. if git_auth "$work_dir" push -u "$push_remote" "$runtime_branch:$runtime_branch"; then
  416. push_ok="1"
  417. log "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch} (set upstream fallback)"
  418. fi
  419. fi
  420. fi
  421. git -C "$work_dir" branch -D "${runtime_branch}-untrimmed" >/dev/null 2>&1 || true
  422. if [[ "$push_ok" != "1" ]]; then
  423. log "[vmess-domain-rotator] git push failed"
  424. log "[vmess-domain-rotator] hint: configure non-interactive auth (credential.helper store, SSH deploy key, or GIT_HTTP_USERNAME/GIT_HTTP_TOKEN)"
  425. if [[ "$push_required" == "1" ]]; then
  426. exit 1
  427. fi
  428. fi
  429. fi
  430. log "[vmess-domain-rotator] committed output changes on ${runtime_branch}: selected value ${after} from ${RUNTIME_DIR}"