添加流水线
This commit is contained in:
57
.gitea/workflows/aliyun-repo-merge.yml
Normal file
57
.gitea/workflows/aliyun-repo-merge.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
name: 阿里云仓库合并流水线(合并模式)
|
||||
|
||||
# 注意:此workflow使用合并模式(git merge --no-ff),会保留目标仓库的更改历史
|
||||
# 如果遇到合并冲突,会跳过该仓库并继续处理其他仓库
|
||||
# 如果需要强制同步(镜像模式),请使用 aliyun-repo-sync.yml workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'aliyun_repos.yaml'
|
||||
- '.gitea/workflows/aliyun-repo-merge.yml'
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# 每天凌晨2点执行
|
||||
- cron: '0 2 * * 0'
|
||||
|
||||
env:
|
||||
CONFIG_FILE: 'aliyun_repos.yaml'
|
||||
|
||||
jobs:
|
||||
merge-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: |
|
||||
apt-get update
|
||||
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.CODEUP_USERNAME }}
|
||||
UPSTREAM_TOKEN: ${{ secrets.CODEUP_PASSWORD }}
|
||||
TARGET_USERNAME: ${{ secrets.TARGET_USERNAME }}
|
||||
TARGET_TOKEN: ${{ secrets.TARGET_TOKEN }}
|
||||
run: python3 merge_repos.py
|
||||
@@ -1,4 +1,7 @@
|
||||
name: 阿里云仓库同步流水线
|
||||
name: 阿里云仓库同步流水线(强制同步/镜像模式)
|
||||
|
||||
# 注意:此workflow使用强制同步模式(git reset --hard),会覆盖目标仓库的所有更改
|
||||
# 如果需要使用合并模式(merge --no-ff),请使用 aliyun-repo-merge.yml workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
317
merge_repos.py
Normal file
317
merge_repos.py
Normal file
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/env python3
|
||||
import yaml
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import shutil
|
||||
import time
|
||||
|
||||
def merge_repository(repo_config):
|
||||
"""合并单个仓库(使用merge --no-ff)"""
|
||||
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
|
||||
|
||||
# 替换环境变量
|
||||
print(f"[DEBUG] ===== 变量替换开始 =====")
|
||||
print(f"[DEBUG] 替换前的 source_url: {source_url}")
|
||||
print(f"[DEBUG] 替换前的 target_url: {target_url}")
|
||||
print(f"[DEBUG] 当前环境变量:")
|
||||
|
||||
# 检查环境变量是否已设置
|
||||
env_vars = {}
|
||||
for var in ['UPSTREAM_USERNAME', 'UPSTREAM_TOKEN', 'TARGET_USERNAME', 'TARGET_TOKEN']:
|
||||
value = os.environ.get(var)
|
||||
env_vars[var] = value
|
||||
if value:
|
||||
print(f"[DEBUG] {var}: 已设置 (长度: {len(value)})")
|
||||
# 隐藏敏感信息(只显示前后几个字符)
|
||||
if 'TOKEN' in var and len(value) > 8:
|
||||
shown = value[:4] + '...' + value[-4:]
|
||||
else:
|
||||
shown = value
|
||||
print(f"[DEBUG] {var} 值: {shown}")
|
||||
else:
|
||||
print(f"[DEBUG] {var}: ⚠️ 未设置或为空!会导致替换失败")
|
||||
|
||||
# 方法1:使用 os.path.expandvars()
|
||||
source_url_expanded = os.path.expandvars(source_url)
|
||||
target_url_expanded = os.path.expandvars(target_url)
|
||||
|
||||
# 方法2:手动替换(备用方案)
|
||||
if '${' in source_url_expanded:
|
||||
print(f"[DEBUG] os.path.expandvars() 未完全替换,尝试手动替换...")
|
||||
for var, value in env_vars.items():
|
||||
if value:
|
||||
source_url_expanded = source_url_expanded.replace(f"${{{var}}}", value)
|
||||
source_url_expanded = source_url_expanded.replace(f"${var}", value)
|
||||
|
||||
if '${' in target_url_expanded:
|
||||
print(f"[DEBUG] os.path.expandvars() 未完全替换,尝试手动替换...")
|
||||
for var, value in env_vars.items():
|
||||
if value:
|
||||
target_url_expanded = target_url_expanded.replace(f"${{{var}}}", value)
|
||||
target_url_expanded = target_url_expanded.replace(f"${var}", value)
|
||||
|
||||
source_url = source_url_expanded
|
||||
target_url = target_url_expanded
|
||||
|
||||
print(f"[DEBUG] 最终 source_url: {source_url}")
|
||||
print(f"[DEBUG] 最终 target_url: {target_url}")
|
||||
print(f"[DEBUG] ===== 变量替换结束 =====")
|
||||
|
||||
# 检查必要的环境变量是否设置
|
||||
if '${' in source_url:
|
||||
print(f"❌ [{name}] 错误: source_url 中的环境变量未完全解析: {source_url}")
|
||||
print(f"❌ 这可能是因为:")
|
||||
print(f" 1. 环境变量未传递给 Python 脚本")
|
||||
print(f" 2. 变量名拼写错误")
|
||||
print(f" 3. secrets 未在 Gitea 中正确配置")
|
||||
return False
|
||||
if '${' in target_url:
|
||||
print(f"❌ [{name}] 错误: target_url 中的环境变量未完全解析: {target_url}")
|
||||
return False
|
||||
|
||||
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"merge_{name}_")
|
||||
try:
|
||||
# 克隆目标仓库
|
||||
print(f"\n[1/7] 克隆目标仓库...")
|
||||
result = subprocess.run(
|
||||
['git', 'clone', target_url, work_dir],
|
||||
capture_output=True, text=True, timeout=3600
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"❌ 克隆目标仓库失败:")
|
||||
print(result.stderr)
|
||||
return False
|
||||
|
||||
os.chdir(work_dir)
|
||||
|
||||
# 添加上游远程
|
||||
print(f"[2/7] 添加上游远程...")
|
||||
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/7] 获取上游更改...")
|
||||
max_retries = 3
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'fetch', 'upstream'],
|
||||
capture_output=True, text=True, timeout=3600
|
||||
)
|
||||
if result.returncode == 0:
|
||||
break
|
||||
print(f"❌ 获取上游更改失败 (尝试 {attempt}/{max_retries}):")
|
||||
print(result.stderr)
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"❌ 获取上游更改超时 (尝试 {attempt}/{max_retries}),200秒限制")
|
||||
|
||||
if attempt < max_retries:
|
||||
print(f"⏳ 等待5秒后重试...")
|
||||
time.sleep(5)
|
||||
else:
|
||||
# 所有尝试都失败
|
||||
print(f"\n网络诊断信息:")
|
||||
print(f"- 错误类型: DNS解析或网络连接失败")
|
||||
print(f"- 可能原因:")
|
||||
print(f" 1. Gitea Actions Runner的DNS配置问题")
|
||||
print(f" 2. Runner容器无法访问外部网络")
|
||||
print(f" 3. 上游仓库服务器防火墙限制")
|
||||
print(f"- 解决方案:")
|
||||
print(f" 1. 在workflow中添加hosts配置")
|
||||
print(f" 2. 检查Runner的网络设置")
|
||||
print(f" 3. 使用IP地址替代域名")
|
||||
return False
|
||||
|
||||
# 检查目标分支是否存在
|
||||
print(f"[4/7] 检查分支...")
|
||||
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/7] 更新本地分支...")
|
||||
subprocess.run(['git', 'fetch', 'origin'], capture_output=True)
|
||||
|
||||
# 检查是否有未提交的更改
|
||||
result = subprocess.run(['git', 'status', '--porcelain'], capture_output=True, text=True)
|
||||
if result.stdout.strip():
|
||||
print(f"❌ 本地仓库有未提交的更改,无法进行合并操作")
|
||||
return False
|
||||
|
||||
# 合并操作:使用 merge --no-ff
|
||||
print(f"[6/7] 合并上游分支 (使用 merge --no-ff)...")
|
||||
result = subprocess.run(
|
||||
['git', 'merge', '--no-ff', f'upstream/{branch}', '-m', f'Merge upstream/{branch} into {branch}'],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"❌ 合并失败,可能存在冲突:")
|
||||
print(result.stderr)
|
||||
|
||||
# 检查是否是合并冲突
|
||||
if "CONFLICT" in result.stderr or "Merge conflict" in result.stderr:
|
||||
print(f"⚠️ 检测到合并冲突,跳过此仓库")
|
||||
# 取消合并状态
|
||||
subprocess.run(['git', 'merge', '--abort'], capture_output=True)
|
||||
return False
|
||||
else:
|
||||
print(f"❌ 合并过程中发生其他错误")
|
||||
return False
|
||||
|
||||
# 推送到目标
|
||||
print(f"[7/7] 推送到目标仓库...")
|
||||
result = subprocess.run(
|
||||
['git', 'push', 'origin', f'refs/heads/{branch}'],
|
||||
capture_output=True, text=True, timeout=3600
|
||||
)
|
||||
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("多仓库合并工具(使用merge --no-ff)")
|
||||
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 = merge_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⚠️ 有仓库合并失败或被跳过,请查看详细日志")
|
||||
# 注意:合并失败不应该导致整个流程失败,因为可能是预期的冲突
|
||||
print("ℹ️ 合并失败通常是预期的冲突,不影响其他仓库的处理")
|
||||
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