init pipeline
This commit is contained in:
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mkdir -p .github/workflows)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
294
.gitea/workflows/multi-repo-sync.yml
Normal file
294
.gitea/workflows/multi-repo-sync.yml
Normal file
@@ -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()
|
||||
180
README.md
Normal file
180
README.md
Normal file
@@ -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)
|
||||
16
repos.yaml
Normal file
16
repos.yaml
Normal file
@@ -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"
|
||||
|
||||
# 添加更多仓库同步配置...
|
||||
Reference in New Issue
Block a user