#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEFAULT_APP_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" APP_DIR="$(git -C "$DEFAULT_APP_DIR" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$DEFAULT_APP_DIR")" force_commit="${GIT_FORCE_COMMIT:-0}" if [[ "${1:-}" == "--force-commit" ]]; then force_commit="1" shift fi if [[ ! "$force_commit" =~ ^[01]$ ]]; then echo "[vmess-domain-rotator] invalid GIT_FORCE_COMMIT=${force_commit}, expected 0 or 1" exit 1 fi CONFIG_PATH="${1:-${APP_DIR}/config.json}" DOMAIN_FILE="${APP_DIR}/runtime/current_domain.txt" export GIT_TERMINAL_PROMPT=0 commit_name="${GIT_COMMIT_NAME:-vmess-domain-rotator}" commit_email="${GIT_COMMIT_EMAIL:-vmess-domain-rotator@localhost}" runtime_branch="${GIT_RUNTIME_BRANCH:-runtime-state}" push_remote="${GIT_PUSH_REMOTE:-origin}" push_enabled="${GIT_PUSH_ENABLED:-1}" push_required="${GIT_PUSH_REQUIRED:-${push_enabled}}" http_user="${GIT_HTTP_USERNAME:-git}" http_token="${GIT_HTTP_TOKEN:-}" http_token_file="${GIT_HTTP_TOKEN_FILE:-}" credential_helper="${GIT_CREDENTIAL_HELPER:-}" ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" if [[ -z "$http_token" ]] && [[ -n "$http_token_file" ]] && [[ -r "$http_token_file" ]]; then http_token="$(tr -d '\r\n' < "$http_token_file")" fi git_auth_opts=() if [[ -n "$http_token" ]]; then auth_b64="$(printf '%s:%s' "$http_user" "$http_token" | base64 | tr -d '\n')" git_auth_opts=(-c "http.extraHeader=Authorization: Basic ${auth_b64}") fi git_auth() { local dir="$1" shift if [[ -n "$credential_helper" ]]; then git -C "$dir" -c credential.helper="$credential_helper" "${git_auth_opts[@]}" "$@" else git -C "$dir" "${git_auth_opts[@]}" "$@" fi } /usr/bin/python3 "${APP_DIR}/scripts/domain_updater.py" --config "$CONFIG_PATH" if [[ ! -f "$DOMAIN_FILE" ]]; then echo "[vmess-domain-rotator] runtime/current_domain.txt missing after updater run, skip git commit" exit 0 fi after="$(tr -d '\r\n' < "$DOMAIN_FILE")" if [[ -z "$after" ]]; then echo "[vmess-domain-rotator] empty selected domain, skip git commit" exit 0 fi if ! command -v git >/dev/null 2>&1; then echo "[vmess-domain-rotator] git not found, skip git commit" exit 0 fi if ! git -C "$APP_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo "[vmess-domain-rotator] not a git repo at ${APP_DIR}, skip git commit" echo "[vmess-domain-rotator] hint: reinstall service from a git clone path" exit 0 fi if ! git -C "$APP_DIR" rev-parse --verify HEAD >/dev/null 2>&1; then echo "[vmess-domain-rotator] repo has no commits yet, skip git commit" exit 0 fi if ! git -C "$APP_DIR" remote get-url "$push_remote" >/dev/null 2>&1; then push_remote="" while IFS= read -r r; do push_remote="$r" break done < <(git -C "$APP_DIR" remote) fi work_dir="" cleanup_worktree="0" current_branch="$(git -C "$APP_DIR" symbolic-ref --quiet --short HEAD 2>/dev/null || true)" if [[ "$current_branch" == "$runtime_branch" ]]; then work_dir="$APP_DIR" else work_dir="$(mktemp -d "${TMPDIR:-/tmp}/vmess-runtime-state.XXXXXX")" cleanup_worktree="1" if git -C "$APP_DIR" show-ref --verify --quiet "refs/heads/${runtime_branch}"; then git -C "$APP_DIR" worktree add --force "$work_dir" "$runtime_branch" else if [[ -n "$push_remote" ]] && git_auth "$APP_DIR" ls-remote --exit-code --heads "$push_remote" "$runtime_branch" >/dev/null 2>&1; then git_auth "$APP_DIR" fetch "$push_remote" "$runtime_branch:$runtime_branch" git -C "$APP_DIR" worktree add --force "$work_dir" "$runtime_branch" else git -C "$APP_DIR" worktree add --force --detach "$work_dir" git -C "$work_dir" checkout --orphan "$runtime_branch" git -C "$work_dir" rm -rf . >/dev/null 2>&1 || true echo "[vmess-domain-rotator] initialized branch ${runtime_branch}" fi fi fi if [[ "$cleanup_worktree" == "1" ]]; then cleanup() { git -C "$APP_DIR" worktree remove --force "$work_dir" >/dev/null 2>&1 || rm -rf "$work_dir" } trap cleanup EXIT fi work_branch="$(git -C "$work_dir" symbolic-ref --quiet --short HEAD 2>/dev/null || true)" if [[ "$work_branch" != "$runtime_branch" ]]; then echo "[vmess-domain-rotator] safety check failed: worktree branch is '${work_branch:-detached}', expected '${runtime_branch}'" exit 1 fi before="" if before_raw="$(git -C "$work_dir" show "HEAD:runtime/current_domain.txt" 2>/dev/null)"; then before="$(printf '%s' "$before_raw" | tr -d '\r\n')" fi if [[ "$force_commit" != "1" ]] && [[ -n "$before" ]] && [[ "$after" == "$before" ]]; then echo "[vmess-domain-rotator] selected domain unchanged (${after}), skip git commit and push" exit 0 fi mkdir -p "$work_dir/runtime" for file in current_domain.txt current_domain.json state.json substore_vars.json; do src="$APP_DIR/runtime/$file" dst="$work_dir/runtime/$file" if [[ -f "$src" ]]; then cp "$src" "$dst" else rm -f "$dst" fi done git -C "$work_dir" add -A runtime/current_domain.txt runtime/current_domain.json runtime/state.json runtime/substore_vars.json || true staged_changed="1" if git -C "$work_dir" diff --cached --quiet; then staged_changed="0" fi if [[ "$staged_changed" == "0" ]] && [[ "$force_commit" != "1" ]]; then echo "[vmess-domain-rotator] no staged changes for ${runtime_branch}, skip git commit" exit 0 fi commit_extra_args=() if [[ "$staged_changed" == "0" ]] && [[ "$force_commit" == "1" ]]; then commit_extra_args+=(--allow-empty) echo "[vmess-domain-rotator] force commit enabled with unchanged content, creating empty commit" fi commit_message="chore: rotate preferred domain to ${after} (${ts})" if [[ "$force_commit" == "1" ]]; then commit_message="manual: domain ${after}, updated at ${ts}" fi git -C "$work_dir" \ -c user.name="$commit_name" \ -c user.email="$commit_email" \ commit "${commit_extra_args[@]}" -m "$commit_message" if [[ "$push_enabled" != "1" ]]; then echo "[vmess-domain-rotator] git push disabled by GIT_PUSH_ENABLED=${push_enabled}" elif [[ -z "$push_remote" ]]; then if [[ "$push_required" == "1" ]]; then echo "[vmess-domain-rotator] no remote found but push is required" exit 1 fi echo "[vmess-domain-rotator] no remote found, skip git push" else push_ok="0" if git -C "$work_dir" rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then if git_auth "$work_dir" push "$push_remote" "$runtime_branch:$runtime_branch"; then push_ok="1" echo "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch}" fi else if git_auth "$work_dir" push -u "$push_remote" "$runtime_branch:$runtime_branch"; then push_ok="1" echo "[vmess-domain-rotator] pushed to ${push_remote}/${runtime_branch} (set upstream)" fi fi if [[ "$push_ok" != "1" ]]; then echo "[vmess-domain-rotator] git push failed" echo "[vmess-domain-rotator] hint: configure non-interactive auth (credential.helper store, SSH deploy key, or GIT_HTTP_USERNAME/GIT_HTTP_TOKEN)" if [[ "$push_required" == "1" ]]; then exit 1 fi fi fi echo "[vmess-domain-rotator] committed runtime changes on ${runtime_branch}: selected domain ${after}"