Files
qianshi/deploy.py
T

126 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Step C: 一键部署 — 本地 commit+push → 服务器 git pull 同步
工作流(根治 git 交叉):
本地 git add/commit/push → 服务器 git pull --ff-only
服务器只做 pull 不做 commit —— pull 只移动引用,不产生新 commit。
这是交叉问题的根源修复:旧流程是 SFTP 上传后服务器再 commit 一次,
同一改动被提交两次,author/committer 时间戳不同 → 同 tree 两个 commit ID
→ ahead 1 behind 1。
依赖(已配置,非交互):
本地: credential.helper=store, ~/.git-credentials
服务器: credential.helper=store, ~/.git-credentials, remote → 127.0.0.1:3000
(Gitea 与服务器同机,走 loopback 避免外部网络波动)
用法: python deploy.py "提交信息"
python deploy.py # 用默认信息
"""
import os
import subprocess
import sys
ROOT = os.path.dirname(os.path.abspath(__file__))
os.chdir(ROOT)
# .gitignore 已排除本机脚本与备份目录,git add -A 会自动跳过,无需额外 pathspec。
# (pathspec 排除 ':!x.py' 在文件已被 .gitignore 忽略时会报错:
# "The following paths are ignored by one of your .gitignore files")
DEFAULT_MSG = 'chore: 部署更新'
SSH_BASE = ("ssh -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
"-o ConnectTimeout=10 ubuntu@106.55.169.92")
REMOTE_DIR = "/var/www/qianshi"
TIMEOUT = 60
ENV = dict(os.environ, GIT_TERMINAL_PROMPT='0') # 禁凭据交互,缺凭据立即失败而非挂起
def sh(args, timeout=TIMEOUT):
"""跑本地命令,返回 (exit, stdout)。args 为 list,shell=False。
Windows 上 shell=True 交给 cmd.exe,引号语义与 POSIX 不同,
提交信息等参数若用 shlex.quote 反而会出错 —— 统一用 list 直传。
"""
r = subprocess.run(args, capture_output=True, text=True,
timeout=timeout, env=ENV)
return r.returncode, (r.stdout + r.stderr).strip()
def log(msg):
print(' %s' % msg, flush=True)
def remote(cmd, timeout=TIMEOUT):
"""在服务器上执行命令,返回 (exit, stdout)
ssh 远程命令必须作为单个 argv 元素传(shell=False),
否则 Windows 的 cmd.exe 会先解析掉引号,ssh 拿不到完整命令。
cd 与 cmd 的拼接在 Python 里完成,不走任何 shell 解析。
"""
args = SSH_BASE.split() + ['cd %s && %s' % (REMOTE_DIR, cmd)]
r = subprocess.run(args, capture_output=True, text=True,
timeout=timeout, env=ENV)
return r.returncode, (r.stdout + r.stderr).strip()
def deploy(msg=DEFAULT_MSG):
log('[1/4] 检查改动')
_, st = sh(['git', 'status', '--porcelain'])
if not st:
log('工作区干净,无需提交')
else:
log('[2/4] 提交')
# .gitignore 已排除本机脚本与备份目录,git add -A 无需额外 pathspec
c, out = sh(['git', 'add', '-A'])
if c:
log('add 失败: %s' % out)
return False
_, staged = sh(['git', 'diff', '--cached', '--name-only'])
if not staged:
log('排除项之外无改动')
else:
log(' %d 个文件' % len(staged.splitlines()))
c, out = sh(['git', 'commit', '-m', msg])
if c:
log('commit 失败: %s' % out)
return False
_, commit = sh(['git', 'rev-parse', '--short', 'HEAD'])
log('本地 HEAD: %s' % commit)
log('[3/4] push origin main')
c, out = sh(['git', 'push', 'origin', 'main'])
if c:
log('push 失败: %s' % out)
return False
log(' pushed')
log('[4/4] 服务器 git pull --ff-only')
c, out = remote('GIT_TERMINAL_PROMPT=0 git pull --ff-only origin main')
if c:
log('pull 失败(若报 diverging,说明服务器产生过本地 commit,需人工处理)')
log(out)
return False
log(' %s' % out.splitlines()[-1] if out else ' ok')
# 校验两端一致(取完整 hash 的最末纯 hash 行,避免 pull 进度信息干扰)
_, local = sh(['git', 'rev-parse', 'HEAD'])
_, srv = remote('git rev-parse HEAD', 30)
srv_hash = next((l for l in srv.splitlines() if len(l) == 40 and all(
c in '0123456789abcdef' for c in l)), '?')
if local != srv_hash:
log('!! 本地 %s != 服务器 %s,同步失败' % (local[:7], srv_hash[:7]))
return False
log(' 服务器 HEAD: %s' % srv_hash[:7])
log('')
log('=== 部署完成 ===')
log('提交: %s %s' % (local[:7], msg))
log('本地/服务器 已同步,工作流无 commit 交叉')
return True
if __name__ == '__main__':
msg = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_MSG
sys.exit(0 if deploy(msg) else 1)