fix
This commit is contained in:
@@ -5,7 +5,9 @@
|
|||||||
"Bash(git pull:*)",
|
"Bash(git pull:*)",
|
||||||
"Bash(git config pull.rebase false:*)",
|
"Bash(git config pull.rebase false:*)",
|
||||||
"Bash(git add .claude/settings.local.json)",
|
"Bash(git add .claude/settings.local.json)",
|
||||||
"Bash(git commit:*)"
|
"Bash(git commit:*)",
|
||||||
|
"Bash(git push:*)",
|
||||||
|
"Bash(/tmp/old_workflow.yml)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
- master
|
- master
|
||||||
paths:
|
paths:
|
||||||
- 'repos.yaml'
|
- 'repos.yaml'
|
||||||
- '.github/workflows/multi-repo-sync.yml'
|
- '.gitea/workflows/*.yml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
# 每天凌晨1点执行
|
# 每天凌晨1点执行
|
||||||
@@ -50,245 +50,4 @@ jobs:
|
|||||||
UPSTREAM_TOKEN: ${{ secrets.UPSTREAM_TOKEN }}
|
UPSTREAM_TOKEN: ${{ secrets.UPSTREAM_TOKEN }}
|
||||||
TARGET_USERNAME: ${{ secrets.TARGET_USERNAME }}
|
TARGET_USERNAME: ${{ secrets.TARGET_USERNAME }}
|
||||||
TARGET_TOKEN: ${{ secrets.TARGET_TOKEN }}
|
TARGET_TOKEN: ${{ secrets.TARGET_TOKEN }}
|
||||||
run: |
|
run: python3 sync_repos.py
|
||||||
#!/usr/bin/env python3
|
|
||||||
import yaml
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
import time
|
|
||||||
|
|
||||||
def sync_repository(repo_config):
|
|
||||||
"""同步单个仓库"""
|
|
||||||
name = repo_config.get('name', 'unnamed-repo')
|
|
||||||
source_url = repo_config.get('source_url', '')
|
|
||||||
target_url = repo_config.get('target_url', '')
|
|
||||||
branch = repo_config.get('branch', 'master')
|
|
||||||
|
|
||||||
if not source_url or not target_url:
|
|
||||||
print(f"❌ [{name}] 错误: 缺少 source_url 或 target_url")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 替换环境变量
|
|
||||||
source_url = os.path.expandvars(source_url)
|
|
||||||
target_url = os.path.expandvars(target_url)
|
|
||||||
|
|
||||||
# 检查必要的环境变量是否设置
|
|
||||||
if '${' in source_url:
|
|
||||||
print(f"⚠️ [{name}] 警告: source_url 中的环境变量未完全解析: {source_url}")
|
|
||||||
if '${' in target_url:
|
|
||||||
print(f"⚠️ [{name}] 警告: target_url 中的环境变量未完全解析: {target_url}")
|
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"开始同步仓库: {name}")
|
|
||||||
print(f"源地址: {source_url.split('@')[-1] if '@' in source_url else source_url}")
|
|
||||||
print(f"目标地址: {target_url.split('@')[-1] if '@' in target_url else target_url}")
|
|
||||||
print(f"分支: {branch}")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
# 创建临时工作目录
|
|
||||||
work_dir = tempfile.mkdtemp(prefix=f"sync_{name}_")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 克隆目标仓库
|
|
||||||
print(f"\n[1/6] 克隆目标仓库...")
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'clone', '--bare', target_url, work_dir],
|
|
||||||
capture_output=True, text=True, timeout=300
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"❌ 克隆目标仓库失败:")
|
|
||||||
print(result.stderr)
|
|
||||||
return False
|
|
||||||
|
|
||||||
os.chdir(work_dir)
|
|
||||||
|
|
||||||
# 添加上游远程
|
|
||||||
print(f"[2/6] 添加上游远程...")
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'remote', 'add', 'upstream', source_url],
|
|
||||||
capture_output=True, text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"❌ 添加上游远程失败:")
|
|
||||||
print(result.stderr)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 获取上游更改
|
|
||||||
print(f"[3/6] 获取上游更改...")
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'fetch', 'upstream'],
|
|
||||||
capture_output=True, text=True, timeout=600
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"❌ 获取上游更改失败:")
|
|
||||||
print(result.stderr)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 检查目标分支是否存在
|
|
||||||
print(f"[4/6] 检查分支...")
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'branch', '-a'],
|
|
||||||
capture_output=True, text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
remote_branch = f'upstream/{branch}'
|
|
||||||
if remote_branch not in result.stdout:
|
|
||||||
print(f"❌ 上游分支 {branch} 不存在!")
|
|
||||||
print(f"可用的分支:")
|
|
||||||
print(result.stdout)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 同步操作:重置到上游(确保完全同步)
|
|
||||||
print(f"[5/6] 同步到上游分支 (使用 reset --hard)...")
|
|
||||||
|
|
||||||
# 首先更新本地分支
|
|
||||||
subprocess.run(['git', 'fetch', 'origin'], capture_output=True)
|
|
||||||
|
|
||||||
# 重置到上游状态
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'reset', '--hard', f'upstream/{branch}'],
|
|
||||||
capture_output=True, text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"❌ 同步失败:")
|
|
||||||
print(result.stderr)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 推送到目标
|
|
||||||
print(f"[6/6] 推送到目标仓库...")
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'push', '--force', '--tags', 'origin', f'refs/heads/{branch}'],
|
|
||||||
capture_output=True, text=True, timeout=600
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"❌ 推送到目标仓库失败:")
|
|
||||||
print(result.stderr)
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"✅ [{name}] 同步成功!")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
print(f"❌ [{name}] 操作超时!")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ [{name}] 发生错误: {e}")
|
|
||||||
return False
|
|
||||||
finally:
|
|
||||||
# 返回原始目录并清理
|
|
||||||
os.chdir('/')
|
|
||||||
if Path(work_dir).exists():
|
|
||||||
shutil.rmtree(work_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
def load_config():
|
|
||||||
"""加载配置文件"""
|
|
||||||
config_path = os.environ.get('CONFIG_FILE', 'repos.yaml')
|
|
||||||
|
|
||||||
if not os.path.exists(config_path):
|
|
||||||
print(f"错误: 配置文件 {config_path} 不存在!")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(config_path, 'r', encoding='utf-8') as f:
|
|
||||||
config = yaml.safe_load(f)
|
|
||||||
|
|
||||||
if not isinstance(config, dict) or 'repositories' not in config:
|
|
||||||
print(f"错误: 配置文件格式不正确,需要包含 'repositories' 键")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
repos = config['repositories']
|
|
||||||
|
|
||||||
if not isinstance(repos, list) or len(repos) == 0:
|
|
||||||
print(f"警告: 配置文件中未找到仓库配置")
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 过滤掉注释掉的或空配置
|
|
||||||
valid_repos = [
|
|
||||||
repo for repo in repos
|
|
||||||
if isinstance(repo, dict) and
|
|
||||||
repo.get('name') and
|
|
||||||
not repo.get('name', '').strip().startswith('#') and
|
|
||||||
(repo.get('source_url') or '').strip()
|
|
||||||
]
|
|
||||||
|
|
||||||
print(f"✓ 找到 {len(valid_repos)} 个仓库配置")
|
|
||||||
return valid_repos
|
|
||||||
|
|
||||||
except yaml.YAMLError as e:
|
|
||||||
print(f"错误: 解析YAML配置文件失败: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"错误: 读取配置文件失败: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("="*80)
|
|
||||||
print("多仓库同步工具")
|
|
||||||
print("="*80)
|
|
||||||
print(f"\n开始时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
||||||
|
|
||||||
# 检查必要的环境变量
|
|
||||||
required_vars = ['UPSTREAM_USERNAME', 'UPSTREAM_TOKEN', 'TARGET_USERNAME', 'TARGET_TOKEN']
|
|
||||||
missing_vars = [var for var in required_vars if not os.environ.get(var)]
|
|
||||||
|
|
||||||
if missing_vars:
|
|
||||||
print(f"\n❌ 错误: 缺少必要的环境变量: {', '.join(missing_vars)}")
|
|
||||||
print("请在 Gitea/Actions Secrets 中设置这些变量")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print("✓ 所有必要环境变量已设置")
|
|
||||||
|
|
||||||
# 加载配置
|
|
||||||
repos = load_config()
|
|
||||||
|
|
||||||
if not repos:
|
|
||||||
print("\n⚠️ 没有需要同步的仓库,退出")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# 同步所有仓库
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for i, repo in enumerate(repos, 1):
|
|
||||||
print(f"\n[{i}/{len(repos)}] 正在同步...")
|
|
||||||
success = sync_repository(repo)
|
|
||||||
results.append((repo['name'], success))
|
|
||||||
|
|
||||||
# 在仓库之间等待一下,避免过于频繁的操作
|
|
||||||
if i < len(repos):
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
# 总结报告
|
|
||||||
print("\n" + "="*80)
|
|
||||||
print("同步完成报告")
|
|
||||||
print("="*80)
|
|
||||||
|
|
||||||
for name, success in results:
|
|
||||||
status = "✅ 成功" if success else "❌ 失败"
|
|
||||||
print(f"{status} {name}")
|
|
||||||
|
|
||||||
successful = sum(1 for _, s in results if s)
|
|
||||||
failed = len(results) - successful
|
|
||||||
|
|
||||||
print(f"\n总计: {len(results)} 个仓库")
|
|
||||||
print(f"成功: {successful} 个")
|
|
||||||
print(f"失败: {failed} 个")
|
|
||||||
|
|
||||||
if failed > 0:
|
|
||||||
print("\n❌ 有仓库同步失败,请查看详细日志")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
print("\n✅ 所有仓库同步成功!")
|
|
||||||
|
|
||||||
print(f"\n结束时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
|
|||||||
193
sync_repos.py
Normal file
193
sync_repos.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import yaml
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
def sync_repository(repo_config):
|
||||||
|
"""同步单个仓库"""
|
||||||
|
name = repo_config.get('name', 'unnamed-repo')
|
||||||
|
source_url = repo_config.get('source_url', '')
|
||||||
|
target_url = repo_config.get('target_url', '')
|
||||||
|
branch = repo_config.get('branch', 'master')
|
||||||
|
if not source_url or not target_url:
|
||||||
|
print(f"❌ [{name}] 错误: 缺少 source_url 或 target_url")
|
||||||
|
return False
|
||||||
|
# 替换环境变量
|
||||||
|
source_url = os.path.expandvars(source_url)
|
||||||
|
target_url = os.path.expandvars(target_url)
|
||||||
|
# 检查必要的环境变量是否设置
|
||||||
|
if '${' in source_url:
|
||||||
|
print(f"⚠️ [{name}] 警告: source_url 中的环境变量未完全解析: {source_url}")
|
||||||
|
if '${' in target_url:
|
||||||
|
print(f"⚠️ [{name}] 警告: target_url 中的环境变量未完全解析: {target_url}")
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"开始同步仓库: {name}")
|
||||||
|
print(f"源地址: {source_url.split('@')[-1] if '@' in source_url else source_url}")
|
||||||
|
print(f"目标地址: {target_url.split('@')[-1] if '@' in target_url else target_url}")
|
||||||
|
print(f"分支: {branch}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
# 创建临时工作目录
|
||||||
|
work_dir = tempfile.mkdtemp(prefix=f"sync_{name}_")
|
||||||
|
try:
|
||||||
|
# 克隆目标仓库
|
||||||
|
print(f"\n[1/6] 克隆目标仓库...")
|
||||||
|
result = subprocess.run(
|
||||||
|
['git', 'clone', '--bare', target_url, work_dir],
|
||||||
|
capture_output=True, text=True, timeout=300
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"❌ 克隆目标仓库失败:")
|
||||||
|
print(result.stderr)
|
||||||
|
return False
|
||||||
|
os.chdir(work_dir)
|
||||||
|
# 添加上游远程
|
||||||
|
print(f"[2/6] 添加上游远程...")
|
||||||
|
result = subprocess.run(
|
||||||
|
['git', 'remote', 'add', 'upstream', source_url],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"❌ 添加上游远程失败:")
|
||||||
|
print(result.stderr)
|
||||||
|
return False
|
||||||
|
# 获取上游更改
|
||||||
|
print(f"[3/6] 获取上游更改...")
|
||||||
|
result = subprocess.run(
|
||||||
|
['git', 'fetch', 'upstream'],
|
||||||
|
capture_output=True, text=True, timeout=600
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"❌ 获取上游更改失败:")
|
||||||
|
print(result.stderr)
|
||||||
|
return False
|
||||||
|
# 检查目标分支是否存在
|
||||||
|
print(f"[4/6] 检查分支...")
|
||||||
|
result = subprocess.run(
|
||||||
|
['git', 'branch', '-a'],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
remote_branch = f'upstream/{branch}'
|
||||||
|
if remote_branch not in result.stdout:
|
||||||
|
print(f"❌ 上游分支 {branch} 不存在!")
|
||||||
|
print(f"可用的分支:")
|
||||||
|
print(result.stdout)
|
||||||
|
return False
|
||||||
|
# 同步操作:重置到上游(确保完全同步)
|
||||||
|
print(f"[5/6] 同步到上游分支 (使用 reset --hard)...")
|
||||||
|
# 首先更新本地分支
|
||||||
|
subprocess.run(['git', 'fetch', 'origin'], capture_output=True)
|
||||||
|
# 重置到上游状态
|
||||||
|
result = subprocess.run(
|
||||||
|
['git', 'reset', '--hard', f'upstream/{branch}'],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"❌ 同步失败:")
|
||||||
|
print(result.stderr)
|
||||||
|
return False
|
||||||
|
# 推送到目标
|
||||||
|
print(f"[6/6] 推送到目标仓库...")
|
||||||
|
result = subprocess.run(
|
||||||
|
['git', 'push', '--force', '--tags', 'origin', f'refs/heads/{branch}'],
|
||||||
|
capture_output=True, text=True, timeout=600
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"❌ 推送到目标仓库失败:")
|
||||||
|
print(result.stderr)
|
||||||
|
return False
|
||||||
|
print(f"✅ [{name}] 同步成功!")
|
||||||
|
return True
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
print(f"❌ [{name}] 操作超时!")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ [{name}] 发生错误: {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
# 返回原始目录并清理
|
||||||
|
os.chdir('/')
|
||||||
|
if Path(work_dir).exists():
|
||||||
|
shutil.rmtree(work_dir, ignore_errors=True)
|
||||||
|
def load_config():
|
||||||
|
"""加载配置文件"""
|
||||||
|
config_path = os.environ.get('CONFIG_FILE', 'repos.yaml')
|
||||||
|
if not os.path.exists(config_path):
|
||||||
|
print(f"错误: 配置文件 {config_path} 不存在!")
|
||||||
|
sys.exit(1)
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
if not isinstance(config, dict) or 'repositories' not in config:
|
||||||
|
print(f"错误: 配置文件格式不正确,需要包含 'repositories' 键")
|
||||||
|
sys.exit(1)
|
||||||
|
repos = config['repositories']
|
||||||
|
if not isinstance(repos, list) or len(repos) == 0:
|
||||||
|
print(f"警告: 配置文件中未找到仓库配置")
|
||||||
|
return []
|
||||||
|
# 过滤掉注释掉的或空配置
|
||||||
|
valid_repos = [
|
||||||
|
repo for repo in repos
|
||||||
|
if isinstance(repo, dict) and
|
||||||
|
repo.get('name') and
|
||||||
|
not repo.get('name', '').strip().startswith('#') and
|
||||||
|
(repo.get('source_url') or '').strip()
|
||||||
|
]
|
||||||
|
print(f"✓ 找到 {len(valid_repos)} 个仓库配置")
|
||||||
|
return valid_repos
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
print(f"错误: 解析YAML配置文件失败: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"错误: 读取配置文件失败: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
def main():
|
||||||
|
print("="*80)
|
||||||
|
print("多仓库同步工具")
|
||||||
|
print("="*80)
|
||||||
|
print(f"\n开始时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
# 检查必要的环境变量
|
||||||
|
required_vars = ['UPSTREAM_USERNAME', 'UPSTREAM_TOKEN', 'TARGET_USERNAME', 'TARGET_TOKEN']
|
||||||
|
missing_vars = [var for var in required_vars if not os.environ.get(var)]
|
||||||
|
if missing_vars:
|
||||||
|
print(f"\n❌ 错误: 缺少必要的环境变量: {', '.join(missing_vars)}")
|
||||||
|
print("请在 Gitea/Actions Secrets 中设置这些变量")
|
||||||
|
sys.exit(1)
|
||||||
|
print("✓ 所有必要环境变量已设置")
|
||||||
|
# 加载配置
|
||||||
|
repos = load_config()
|
||||||
|
if not repos:
|
||||||
|
print("\n⚠️ 没有需要同步的仓库,退出")
|
||||||
|
sys.exit(0)
|
||||||
|
# 同步所有仓库
|
||||||
|
results = []
|
||||||
|
for i, repo in enumerate(repos, 1):
|
||||||
|
print(f"\n[{i}/{len(repos)}] 正在同步...")
|
||||||
|
success = sync_repository(repo)
|
||||||
|
results.append((repo['name'], success))
|
||||||
|
# 在仓库之间等待一下,避免过于频繁的操作
|
||||||
|
if i < len(repos):
|
||||||
|
time.sleep(5)
|
||||||
|
# 总结报告
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("同步完成报告")
|
||||||
|
print("="*80)
|
||||||
|
for name, success in results:
|
||||||
|
status = "✅ 成功" if success else "❌ 失败"
|
||||||
|
print(f"{status} {name}")
|
||||||
|
successful = sum(1 for _, s in results if s)
|
||||||
|
failed = len(results) - successful
|
||||||
|
print(f"\n总计: {len(results)} 个仓库")
|
||||||
|
print(f"成功: {successful} 个")
|
||||||
|
print(f"失败: {failed} 个")
|
||||||
|
if failed > 0:
|
||||||
|
print("\n❌ 有仓库同步失败,请查看详细日志")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print("\n✅ 所有仓库同步成功!")
|
||||||
|
print(f"\n结束时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user