From f74f06807128f0fbb6bb74f7c64bcf75e608cab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=E5=98=89=E9=98=B3-coder?= Date: Thu, 20 Nov 2025 21:35:08 +0800 Subject: [PATCH] init pipeline --- .claude/settings.local.json | 9 + .gitea/workflows/multi-repo-sync.yml | 294 +++++++++++++++++++++++++++ README.md | 180 ++++++++++++++++ repos.yaml | 16 ++ 4 files changed, 499 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .gitea/workflows/multi-repo-sync.yml create mode 100644 README.md create mode 100644 repos.yaml diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..7d5346a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(mkdir -p .github/workflows)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.gitea/workflows/multi-repo-sync.yml b/.gitea/workflows/multi-repo-sync.yml new file mode 100644 index 0000000..e4de1c4 --- /dev/null +++ b/.gitea/workflows/multi-repo-sync.yml @@ -0,0 +1,294 @@ +name: 多仓库同步流水线 + +on: + push: + branches: + - master + paths: + - 'repos.yaml' + - '.github/workflows/multi-repo-sync.yml' + workflow_dispatch: + schedule: + # 每天凌晨1点执行 + - cron: '0 1 * * *' + +env: + CONFIG_FILE: 'repos.yaml' + +jobs: + sync-repositories: + runs-on: ubuntu-latest + + steps: + - name: 签出配置仓库 + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: 配置 Git + run: | + git config --global user.name "gitea-runner" + git config --global user.email "actions@gitea.fjy8018.top" + git config --global init.defaultBranch master + + - name: 安装依赖 + run: | + sudo apt-get update + sudo apt-get install -y python3-yaml jq + + - name: 验证配置文件 + run: | + if [ ! -f "${{ env.CONFIG_FILE }}" ]; then + echo "错误: 配置文件 ${{ env.CONFIG_FILE }} 不存在!" + exit 1 + fi + echo "✓ 配置文件已找到" + + - name: 从配置文件中提取仓库并同步 + env: + UPSTREAM_USERNAME: ${{ secrets.UPSTREAM_USERNAME }} + UPSTREAM_TOKEN: ${{ secrets.UPSTREAM_TOKEN }} + GITEA_USERNAME: ${{ secrets.GITEA_USERNAME }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + #!/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', 'GITEA_USERNAME', 'GITEA_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() diff --git a/README.md b/README.md new file mode 100644 index 0000000..b04a103 --- /dev/null +++ b/README.md @@ -0,0 +1,180 @@ +# 多仓库同步系统 + +这是一个支持批量同步多个 Git 仓库的自动化流水线系统,适用于从上游仓库同步代码到目标 Gitea 仓库。 + +## 🚀 功能特性 + +- **批量同步**:通过配置文件一次性管理多个仓库同步 +- **冲突自动处理**:使用 `reset --hard` 策略,确保目标仓库完全同步为上游状态 +- **定时执行**:支持定时任务(每天凌晨1点自动执行) +- **手动触发**:支持手动触发和配置文件更新时自动触发 +- **详细日志**:提供每个仓库同步过程的详细日志和总结报告 +- **安全可靠**:内置超时机制和错误处理 + +## 📁 项目结构 + +``` +. +├── repos.yaml # 仓库同步配置文件 +├── .gitea/ +│ └── workflows/ +│ └── multi-repo-sync.yml # 同步流水线定义 +└── README.md # 本文档 +``` + +**注意:** Gitea Actions 支持两种工作流路径: +- `.gitea/workflows/` (官方推荐) +- `.github/workflows/` (兼容性支持) + +## 🔧 配置说明 + +### 1. 配置文件 (repos.yaml) + +在项目根目录的 `repos.yaml` 文件中定义需要同步的仓库: + +```yaml +# 多仓库同步配置文件 +# 定义需要同步的仓库对,源仓库 -> 目标仓库 +repositories: + # 示例:同步 BladeX-Tool 仓库 + - name: "bladex-tool" # 仓库名称(用于日志输出) + source_url: "https://${UPSTREAM_USERNAME}:${UPSTREAM_TOKEN}@center.javablade.com/blade/BladeX-Tool.git" + target_url: "https://${GITEA_USERNAME}:${GITEA_TOKEN}@gitea.fjy8018.top/home/BladeX-Tool.git" + branch: "master" # 要同步的分支 + + # 示例:同步另一个仓库(取消注释并修改以下配置) + - name: "another-repo" + source_url: "https://${UPSTREAM_USERNAME}:${UPSTREAM_TOKEN}@github.com/example/repo.git" + target_url: "https://${GITEA_USERNAME}:${GITEA_TOKEN}@gitea.fjy8018.top/home/repo.git" + branch: "main" + + # 添加更多仓库同步配置... +``` + +**配置字段说明:** + +| 字段 | 必填 | 说明 | +|------|------|------| +| `name` | 是 | 仓库标识名称,用于日志输出 | +| `source_url` | 是 | 上游仓库地址,支持环境变量 | +| `target_url` | 是 | 目标 Gitea 仓库地址,支持环境变量 | +| `branch` | 否 | 要同步的分支,默认为 `master` | + +### 2. 环境变量 + +在 Gitea 仓库的 **Settings > Secrets** 中配置以下变量: + +| 变量名 | 必填 | 说明 | +|--------|------|------| +| `UPSTREAM_USERNAME` | 是 | 上游仓库用户名 | +| `UPSTREAM_TOKEN` | 是 | 上游仓库访问令牌/密码 | +| `GITEA_USERNAME` | 是 | 目标 Gitea 用户名 | +| `GITEA_TOKEN` | 是 | 目标 Gitea 访问令牌 | + +**注意:** 建议使用 Personal Access Token (PAT) 而不是密码,以提高安全性。 + +## 🚀 使用方法 + +### 方法 1:手动触发 + +进入 Gitea 仓库页面的 **Actions** 标签页,选择 "多仓库同步流水线",点击 **Run workflow** 手动执行。 + +### 方法 2:定时任务 + +流水线默认配置为每天凌晨 1 点(UTC)自动执行。可在 `.gitea/workflows/multi-repo-sync.yml` 文件中修改 cron 表达式: + +```yaml +schedule: + - cron: '0 1 * * *' # 每天凌晨1点执行 +``` + +### 方法 3:配置文件更新时自动触发 + +当修改 `repos.yaml` 或工作流文件时,会自动触发同步。 + +## 📊 执行结果 + +执行成功后,可以在 Gitea Actions 页面查看: + +- 每个仓库的同步状态(成功/失败) +- 详细的同步日志 +- 同步完成总结报告 + +示例输出: +``` +============================================================ +开始同步仓库: bladex-tool +源地址: center.javablade.com/blade/BladeX-Tool.git +目标地址: gitea.fjy8018.top/home/BladeX-Tool.git +分支: master +============================================================ + +[1/6] 克隆目标仓库... +[2/6] 添加上游远程... +[3/6] 获取上游更改... +[4/6] 检查分支... +[5/6] 同步到上游分支... +[6/6] 推送到目标仓库... +✅ [bladex-tool] 同步成功! +``` + +## ⚠️ 重要说明 + +1. **强制同步策略**:本流水线使用 `git reset --hard` 策略,确保目标仓库**完全与上游一致**。任何在目标仓库上的修改都可能被覆盖。 + +2. **权限要求**:确保提供的 Token 具有对目标仓库的写入权限。 + +3. **冲突处理**:由于采用强制重置策略,不会保留目标仓库与上游的冲突修改。 + +4. **大仓库同步**:对于大型仓库,可能需要调整流水线超时时间。 + +5. **私有仓库**:需要确保 Token 具有访问私有仓库的权限。 + +## 🔍 故障排除 + +### 问题:权限被拒绝(Permission denied) + +**解决方案**: +- 检查 Secrets 中的用户名和 Token 是否正确 +- 确认 Token 具有 repo 范围的权限 +- 验证仓库 URL 是否正确 + +### 问题:仓库不存在 + +**解决方案**: +- 在目标 Gitea 上预先创建空仓库 +- 检查配置文件中的仓库地址是否正确 + +### 问题:同步超时 + +**解决方案**: +- 在 `.gitea/workflows/multi-repo-sync.yml` 中增加超时时间 +- 对于大仓库,考虑使用浅克隆(已优化为 bare clone) + +### 问题:配置文件解析错误 + +**解决方案**: +- 使用 YAML 格式验证工具检查 repos.yaml +- 确保所有必要字段都已填写 + +## 📝 最佳实践 + +1. **逐步添加仓库**:初次使用时,先配置 1-2 个仓库测试 +2. **使用变量**:将敏感信息和可变信息放在 Secrets 中 +3. **监控执行**:定期检查流水线执行日志 +4. **测试环境**:先在测试仓库验证同步逻辑 +5. **备份策略**:重要仓库确保有备份机制 + +## 🤝 贡献 + +欢迎提交 Issue 和 Pull Request 来改进此项目。 + +## 📄 许可证 + +MIT License + +## 🔗 相关链接 + +- [Gitea Actions 文档](https://docs.gitea.com/usage/actions/overview) +- [GitHub Actions 工作流语法](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) +- [Git 官方文档](https://git-scm.com/doc) diff --git a/repos.yaml b/repos.yaml new file mode 100644 index 0000000..12fe45a --- /dev/null +++ b/repos.yaml @@ -0,0 +1,16 @@ +# 多仓库同步配置文件 +# 定义需要同步的仓库对,源仓库 -> 目标仓库 +repositories: + # 示例:同步 BladeX-Tool 仓库 + - name: "bladex-tool" # 仓库名称(用于日志输出) + source_url: "https://${UPSTREAM_USERNAME}:${UPSTREAM_TOKEN}@center.javablade.com/blade/BladeX-Tool.git" + target_url: "https://${GITEA_USERNAME}:${GITEA_TOKEN}@gitea.fjy8018.top/home/BladeX-Tool.git" + branch: "master" # 要同步的分支 + + # 示例:同步另一个仓库(取消注释并修改以下配置) + # - name: "another-repo" + # source_url: "https://${UPSTREAM_USERNAME}:${UPSTREAM_TOKEN}@github.com/example/repo.git" + # target_url: "https://${GITEA_USERNAME}:${GITEA_TOKEN}@gitea.fjy8018.top/home/repo.git" + # branch: "main" + + # 添加更多仓库同步配置...