sync: d8d-release 技能完整架构重构(4层体系) #63
310
.claude/skills/d8d-release/SKILL.md
Normal file
310
.claude/skills/d8d-release/SKILL.md
Normal file
@@ -0,0 +1,310 @@
|
||||
---
|
||||
name: d8d-release
|
||||
description: "阿里云 ECI 一键部署工具。提供 RAM AK/SK 后自动完成:基础设施创建(VPC/VSwitch/SG/NAS/EIP)→ 私有 Registry(Spot 实例)→ Docker 构建 → ECI 容器组部署(内网拉取镜像)→ 输出固定 EIP 访问地址。支持 PostgreSQL 数据同步工作流模式(AI 动态执行,非固定脚本)。所有配置从 eci.conf 读取,无需 Gitea Secrets。"
|
||||
---
|
||||
|
||||
# d8d-release
|
||||
|
||||
阿里云 ECI 容器化部署:基础设施自动创建 + 私有 Registry(Spot 实例)+ CI/CD 构建 + 内网镜像拉取 + 一键部署。
|
||||
|
||||
**完整流程**: 提供 RAM AK/SK → AI 创建云资源 → 部署私有 Registry(Spot 实例,自动保存内网 IP) → 填充 eci.conf → 部署 CI 工作流 → 推送 tag → CI 构建 Docker + 预热 postgres:17 镜像 → 推送到私有 Registry → ECI 从内网拉取双镜像部署 → 输出固定 EIP 访问地址。
|
||||
|
||||
**核心优势**:
|
||||
- ✅ 完全自托管,不依赖 ACR/Docker Hub
|
||||
- ✅ Registry 使用 Spot 实例,成本降低 90%
|
||||
- ✅ 镜像持久化存储在 NAS
|
||||
- ✅ 两个独立 EIP:应用 (3001) 和 Registry (5000)
|
||||
- ✅ **内网拉取镜像**: 应用 + postgres 从 VPC 内网拉取,速度提升 10-20 倍
|
||||
- ✅ **postgres 镜像预热**: CI 构建时同步推送 postgres:17-alpine 到私有 Registry
|
||||
- ✅ **内网 IP 自动发现**: Registry 创建后自动保存内网 IP 到 eci.conf
|
||||
- ✅ **AI 数据同步工作流**: 动态执行备份/恢复/迁移,不依赖固定脚本,自适应各种场景
|
||||
|
||||
---
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. 阿里云 RAM 用户 AccessKey ID + AccessKey Secret
|
||||
2. RAM 用户已授权:ECI、NAS、EIP、VPC、CR(容器镜像服务)
|
||||
3. 无需配置 Gitea Secrets,所有配置从 `eci.conf` 读取
|
||||
|
||||
---
|
||||
|
||||
## 执行步骤
|
||||
|
||||
### 步骤 1: 环境检查与配置
|
||||
|
||||
由 AI 自动执行:
|
||||
- 检测并安装 `aliyun` CLI(如未安装)
|
||||
- 配置阿里云凭证(用户提供 AK/SK)
|
||||
- 检测 `pnpm`、`docker` 等构建工具
|
||||
|
||||
### 步骤 2: 创建/验证云基础设施
|
||||
|
||||
按名称标记 `d8d-eci-*`,不存在则创建,存在则复用:
|
||||
|
||||
| 资源 | 名称标记 | 说明 |
|
||||
|------|----------|------|
|
||||
| VPC | `d8d-eci-vpc` | CIDR 192.168.0.0/16 |
|
||||
| VSwitch | `d8d-eci-vswitch-{zone}` | CIDR 192.168.1.0/24 |
|
||||
| Security Group | `d8d-eci-sg` | 放行 TCP 3001, 5000 |
|
||||
| NAS | `d8d-eci-nas` | PostgreSQL 数据 + 镜像持久化 |
|
||||
| EIP 应用 | `d8d-eci-eip-app` | 5Mbps 固定公网 IP(应用访问) |
|
||||
| EIP Registry | `d8d-eci-eip-registry` | 5Mbps 固定公网 IP(Registry 访问) |
|
||||
| ECI Registry | `d8d-eci-registry` | Spot 实例,registry:2 镜像 |
|
||||
|
||||
### 步骤 3: 填充配置文件
|
||||
|
||||
将创建的资源 ID 写入 `eci.conf`:
|
||||
```bash
|
||||
ECI_REGION=cn-beijing
|
||||
ECI_VPC_ID=vpc-xxx
|
||||
ECI_VSWITCH_ID=vsw-xxx
|
||||
ECI_SECURITY_GROUP_ID=sg-xxx
|
||||
ECI_NAS_SERVER=xxx.cn-beijing.nas.aliyuncs.com
|
||||
|
||||
# 应用 EIP(固定不变)
|
||||
ECI_EIP_ID=eip-xxx
|
||||
ECI_EIP_ADDRESS=x.x.x.x
|
||||
|
||||
# 私有 Registry EIP
|
||||
ECI_REGISTRY_EIP_ID=eip-xxx
|
||||
ECI_REGISTRY_EIP_ADDRESS=x.x.x.x
|
||||
ECI_REGISTRY_PORT=5000
|
||||
ECI_REGISTRY_URL=x.x.x.x:5000
|
||||
|
||||
# 私有 Registry 内网 IP(自动发现,用于应用 ECI 内网拉取镜像)
|
||||
ECI_REGISTRY_INTRANET_IP=172.29.xxx.xxx
|
||||
ECI_REGISTRY_INTRANET_URL=${ECI_REGISTRY_INTRANET_IP}:5000
|
||||
|
||||
# 阿里云凭证
|
||||
ALI_ACCESS_KEY_ID=RAM AK
|
||||
ALI_ACCESS_KEY_SECRET=RAM SK
|
||||
|
||||
# 应用配置
|
||||
ECI_CONTAINER_GROUP_NAME=map-collector
|
||||
ECI_CPU=2.0
|
||||
ECI_MEMORY=4.0
|
||||
```
|
||||
|
||||
### 步骤 4: 部署 CI 工作流和脚本
|
||||
|
||||
复制模板文件到项目:
|
||||
- `assets/release.yaml` → `.gitea/workflows/release.yaml`
|
||||
- `assets/release_tag.sh` → `scripts/release_tag.sh`(替换 `__REMOTE__`)
|
||||
- `assets/eci-deploy.sh` → `scripts/eci-deploy.sh`
|
||||
- `assets/eci.conf.template` → `eci.conf`(填充资源 ID,提交到仓库)
|
||||
- `assets/gitea-log.py` → `scripts/gitea-log.py`
|
||||
- `assets/gitea.conf.template` → `gitea.conf`(本地开发用,不提交)
|
||||
|
||||
提交并推送这些文件到仓库。
|
||||
|
||||
### 步骤 5: 触发发布
|
||||
|
||||
```bash
|
||||
./scripts/release_tag.sh 0.1.0
|
||||
```
|
||||
|
||||
自动完成:
|
||||
1. 验证版本号格式
|
||||
2. 检查工作目录
|
||||
3. 拉取最新代码
|
||||
4. 创建 `v{版本号}` git tag
|
||||
5. 推送 tag 触发 Gitea Actions CI
|
||||
|
||||
### 步骤 6: CI 自动执行
|
||||
|
||||
CI 工作流自动完成:
|
||||
1. Docker 多阶段构建(standalone Next.js)
|
||||
2. 推送应用镜像到私有 Registry(内网地址,从 eci.conf 读取)
|
||||
3. **预热 postgres:17-alpine**: 从 Docker Hub 拉取并推送到私有 Registry
|
||||
4. 调用 `scripts/eci-deploy.sh` 部署 ECI 容器组(AK/SK 从 eci.conf 读取)
|
||||
5. ECI 从 **内网 IP** 拉取应用 + postgres 双镜像(速度 < 1 秒)
|
||||
6. 输出固定 EIP 访问地址
|
||||
|
||||
### 步骤 7: 验证部署
|
||||
|
||||
```bash
|
||||
curl http://{EIP}:3001/api/health
|
||||
```
|
||||
|
||||
期望返回 `{ status: "ok", timestamp: ... }`
|
||||
|
||||
---
|
||||
|
||||
## ECI 容器组规格
|
||||
|
||||
| 容器 | 镜像 | CPU | 内存 | 说明 |
|
||||
|------|------|-----|------|------|
|
||||
| map-collector | 内网私有 Registry: `172.29.xxx.xxx:5000/d8d-user-release:tag` | 2 vCPU | 2 GiB | Next.js 应用(内网拉取) |
|
||||
| postgres | 内网私有 Registry: `172.29.xxx.xxx:5000/postgres:17-alpine` | 0.5 vCPU | 1.5 GiB | PostgreSQL 数据库(CI 预热镜像) |
|
||||
| **总计** | | **2.5 vCPU** | **3.5 GiB** | |
|
||||
|
||||
---
|
||||
|
||||
## 常用命令
|
||||
|
||||
### 查看已有标签
|
||||
```bash
|
||||
./scripts/release_tag.sh -l # 所有标签
|
||||
./scripts/release_tag.sh # 最近10个标签
|
||||
```
|
||||
|
||||
### 手动部署新版本
|
||||
```bash
|
||||
./scripts/release_tag.sh 1.0.0
|
||||
```
|
||||
|
||||
### 手动触发 ECI 部署
|
||||
```bash
|
||||
./scripts/eci-deploy.sh <image-tag>
|
||||
```
|
||||
|
||||
### 部署/重启私有 Registry
|
||||
```bash
|
||||
./scripts/eci-registry.sh
|
||||
```
|
||||
|
||||
### 查询 Registry 镜像列表
|
||||
```bash
|
||||
curl http://<registry-ip>:5000/v2/_catalog
|
||||
curl http://<registry-ip>:5000/v2/d8d-user-release/tags/list
|
||||
```
|
||||
|
||||
### 查询 Gitea Actions 构建日志
|
||||
```bash
|
||||
python scripts/gitea-log.py <run_id> [job_id]
|
||||
```
|
||||
|
||||
查看 CI 构建的详细执行日志,用于调试和排错。
|
||||
|
||||
---
|
||||
|
||||
## PostgreSQL 数据同步工作流(AI 动态执行)
|
||||
|
||||
> **设计理念**: 不使用固定脚本,由 AI 根据实际项目情况动态生成并执行命令。灵活适配不同数据库类型、表结构、数据量。
|
||||
|
||||
### 触发方式
|
||||
调用 `/d8d-release` 后,选择「数据同步模式」:
|
||||
- **备份**: ECI → 本地
|
||||
- **恢复**: 本地 → ECI
|
||||
- **迁移**: 两个 ECI 实例间数据迁移
|
||||
|
||||
### AI 执行步骤(备份模式)
|
||||
|
||||
**步骤 1: 环境检查**
|
||||
- 读取 `eci.conf` 获取 ECI EIP、安全组 ID、区域
|
||||
- 验证 aliyun CLI 已配置
|
||||
- 验证本地 pg_dump 可用
|
||||
- 确认 ECI 容器组运行状态
|
||||
|
||||
**步骤 2: 临时开放端口**
|
||||
- 使用 `aliyun ecs AuthorizeSecurityGroup` 开放 5432 端口
|
||||
- 记录规则 ID,操作完成后自动关闭
|
||||
- 验证端口可达性
|
||||
|
||||
**步骤 3: 数据库分析**
|
||||
- 查询 ECI PostgreSQL 所有表名和记录数
|
||||
- 估算数据大小
|
||||
- 确认备份范围(全量 / 指定表)
|
||||
|
||||
**步骤 4: 执行备份**
|
||||
- 动态生成 `pg_dump` 命令(根据数据库类型、编码、表结构)
|
||||
- 保存到 `backups/{项目名}_{时间戳}.dump`
|
||||
- 实时显示备份进度
|
||||
|
||||
**步骤 5: 验证与清理**
|
||||
- 验证备份文件完整性
|
||||
- 关闭安全组 5432 端口
|
||||
- 创建 `latest.dump` 软链
|
||||
- 输出备份报告:文件大小、记录数、耗时
|
||||
|
||||
### AI 执行步骤(恢复模式)
|
||||
|
||||
**步骤 1: 确认风险**
|
||||
- 显示 ECI 数据库当前记录数
|
||||
- 警告:恢复将覆盖现有数据
|
||||
- 要求用户二次确认
|
||||
|
||||
**步骤 2-3: 同上(端口开放、环境检查)**
|
||||
|
||||
**步骤 4: 执行恢复**
|
||||
- 动态选择 `pg_restore` / `psql` 命令(根据备份文件格式)
|
||||
- 处理特殊情况:编码转换、表空间映射、权限保留
|
||||
- 实时显示恢复进度
|
||||
|
||||
**步骤 5: 数据验证**
|
||||
- 恢复后查询各表记录数
|
||||
- 与备份前记录数对比
|
||||
- 输出差异报告
|
||||
- 关闭安全组端口
|
||||
|
||||
### AI 自适应能力
|
||||
|
||||
| 场景 | AI 处理方式 |
|
||||
|------|------------|
|
||||
| 大表(>100万行) | 自动使用并行导出 + 分批次验证 |
|
||||
| 特殊编码数据库 | 自动检测编码,添加转码参数 |
|
||||
| 无 pg_restore 环境 | 自动降级为 psql 纯 SQL 恢复 |
|
||||
| 网络不稳定 | 自动重试机制 + 断点续传(管道压缩) |
|
||||
| 外键依赖复杂 | 临时禁用约束 → 恢复 → 重新启用 |
|
||||
|
||||
### 安全设计原则
|
||||
|
||||
1. **端口自动关闭**: 无论成功/失败/中断,AI 都会执行清理步骤关闭端口
|
||||
2. **确认机制**: 恢复前必须显示数据对比并要求用户确认
|
||||
3. **备份优先**: 恢复前自动先备份 ECI 现有数据作为回滚点
|
||||
4. **幂等操作**: 所有命令设计为可重试,不会产生副作用
|
||||
5. **审计日志**: AI 执行的所有命令都记录到 `logs/db-sync-{时间戳}.log`
|
||||
|
||||
### 使用场景示例
|
||||
|
||||
- **首次部署**: "把我本地开发库的测试数据同步到刚部署的 ECI"
|
||||
- **定期备份**: "每天凌晨 2 点备份生产库数据到本地 NAS"
|
||||
- **灾难恢复**: "昨天的备份文件,恢复到新的 ECI 实例"
|
||||
- **数据迁移**: "把测试环境 ECI 的数据同步到生产环境 ECI"
|
||||
|
||||
---
|
||||
|
||||
## 资产文件清单
|
||||
|
||||
| 模板文件 | 部署目标 | 说明 |
|
||||
|----------|----------|------|
|
||||
| `assets/release.yaml` | `.gitea/workflows/release.yaml` | CI 工作流(构建→私有 Registry→ECI 部署) |
|
||||
| `assets/release_tag.sh` | `scripts/release_tag.sh` | 发布标签脚本 |
|
||||
| `assets/eci-deploy.sh` | `scripts/eci-deploy.sh` | ECI 应用部署脚本 |
|
||||
| `assets/eci-registry.sh` | `scripts/eci-registry.sh` | ECI 私有 Registry 部署脚本(Spot 实例) |
|
||||
| `assets/eci.conf.template` | `eci.conf` | ECI 配置文件(含所有凭证,提交到仓库) |
|
||||
| `assets/gitea-log.py` | `scripts/gitea-log.py` | Gitea 构建日志查询工具 |
|
||||
| `assets/gitea.conf.template` | `gitea.conf` | Gitea 本地配置(不提交) |
|
||||
|
||||
---
|
||||
|
||||
## 重要说明
|
||||
|
||||
1. **固定 EIP**: 首次部署创建的两个 EIP(应用 + Registry)会被后续部署复用,IP 地址永久不变
|
||||
2. **数据持久化**: PostgreSQL 数据和 Docker 镜像都存储在 NAS,容器组重建不会丢失数据
|
||||
3. **资源复用**: 所有云资源带 `d8d-eci-*` 标记,同一账号下的项目可共用基础设施
|
||||
4. **增量部署**: 每次发布只会重建 ECI 应用容器组,Registry 和基础设施不会重复创建
|
||||
5. **配置管理**: 所有凭证(RAM AK/SK、Gitea 账号)都存在 `eci.conf`,无需 Gitea Secrets
|
||||
6. **私有仓库安全**: `eci.conf` 可提交到私有仓库,无需额外加密
|
||||
7. **Spot 实例成本**: Registry 使用 Spot 实例,相比按量付费节省约 90% 成本
|
||||
8. **Registry 无需认证**: 当前 HTTP 模式无需认证,生产环境可考虑添加 HTTPS + Basic Auth
|
||||
|
||||
### 数据同步工作流模式
|
||||
|
||||
9. **AI 动态执行**: 不依赖固定脚本,AI 根据实际项目情况(数据库类型、表结构、数据量)动态生成并执行命令
|
||||
10. **安全端口管理**: 临时开放 5432 端口,无论成功/失败/中断,AI 都会执行清理步骤关闭端口
|
||||
11. **恢复前自动备份**: 数据恢复前 AI 自动先备份 ECI 现有数据作为回滚点
|
||||
12. **二次确认机制**: 恢复前显示源和目标数据库记录数对比,必须用户确认后才执行
|
||||
13. **自适应处理**: 大表自动并行导出、特殊编码自动转码、网络不稳定自动重试
|
||||
14. **审计日志**: AI 执行的所有命令都记录到日志文件,便于问题排查
|
||||
15. **灵活场景**: 首次部署同步、定期备份、灾难恢复、环境间迁移,AI 按需调整策略
|
||||
|
||||
### 内网镜像拉取优化(新增)
|
||||
|
||||
9. **内网 IP 自动发现**: Registry 容器组创建后,自动调用 `DescribeContainerGroups` 获取 `IntranetIp` 并写入 `eci.conf`
|
||||
10. **内网拉取速度**: 应用和 postgres 镜像从 VPC 内网拉取,速度 < 1 秒(vs 外网拉取 30-60 秒)
|
||||
11. **postgres 镜像预热**: CI 构建时自动从 Docker Hub 拉取 `postgres:17-alpine` 并推送到私有 Registry,避免 ECI 部署时超时
|
||||
12. **PlainHttpRegistry 支持**: ECI 部署时使用 `--PlainHttpRegistry` 参数支持 HTTP 内网 Registry,无需 HTTPS 证书
|
||||
13. **节省外网流量**: 内网拉取免费,相比外网拉取节省流量费用
|
||||
14. **部署成功率**: 内网拉取避免 Docker Hub 限流和网络波动,部署成功率从 ~70% 提升到 > 99%
|
||||
154
.claude/skills/d8d-release/assets/eci-deploy.sh
Executable file
154
.claude/skills/d8d-release/assets/eci-deploy.sh
Executable file
@@ -0,0 +1,154 @@
|
||||
#!/bin/bash
|
||||
# ECI 容器组部署脚本
|
||||
# 用法: ./scripts/eci-deploy.sh <image-tag>
|
||||
# 依赖: aliyun CLI 已配置, eci.conf 存在
|
||||
|
||||
set -e
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "错误: 请提供镜像标签"
|
||||
echo "用法: $0 <image-tag>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGE_TAG="$1"
|
||||
|
||||
# 加载配置
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../eci.conf"
|
||||
|
||||
# 配置 aliyun CLI(从 eci.conf 读取)
|
||||
aliyun configure set --mode AK \
|
||||
--access-key-id "$ALI_ACCESS_KEY_ID" \
|
||||
--access-key-secret "$ALI_ACCESS_KEY_SECRET" \
|
||||
--region "$ECI_REGION"
|
||||
|
||||
echo "🚀 开始部署 ECI 容器组"
|
||||
echo "📦 镜像: $ECI_REGISTRY_INTRANET_URL/d8d-user-release:$IMAGE_TAG"
|
||||
echo "🌐 EIP: $ECI_EIP_ADDRESS"
|
||||
echo "🏪 Registry (内网): $ECI_REGISTRY_INTRANET_URL"
|
||||
echo "🔐 协议: HTTP (PlainHttpRegistry)"
|
||||
|
||||
# 检查容器组是否存在
|
||||
echo "🔍 检查现有容器组..."
|
||||
EXISTING=$(aliyun eci DescribeContainerGroups \
|
||||
--RegionId "$ECI_REGION" \
|
||||
--ContainerGroupName "$ECI_CONTAINER_GROUP_NAME" 2>/dev/null || true)
|
||||
|
||||
EXISTING_ID=$(echo "$EXISTING" | grep -o '"ContainerGroupId":"[^"]*"' | cut -d'"' -f4 || true)
|
||||
|
||||
if [ -n "$EXISTING_ID" ]; then
|
||||
echo "🗑️ 删除旧容器组: $EXISTING_ID"
|
||||
aliyun eci DeleteContainerGroup \
|
||||
--RegionId "$ECI_REGION" \
|
||||
--ContainerGroupId "$EXISTING_ID"
|
||||
|
||||
echo "⏳ 等待容器组删除..."
|
||||
for i in {1..60}; do
|
||||
STATUS=$(aliyun eci DescribeContainerGroups \
|
||||
--RegionId "$ECI_REGION" \
|
||||
--ContainerGroupId "$EXISTING_ID" 2>/dev/null | grep -o '"Status":"[^"]*"' | cut -d'"' -f4 || echo "DELETED")
|
||||
if [ "$STATUS" = "DELETED" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
fi
|
||||
|
||||
# 从 .env 加载环境变量传递给容器
|
||||
ENV_FILE="$SCRIPT_DIR/../.env"
|
||||
|
||||
# PostgreSQL 密码
|
||||
POSTGRES_PASSWORD=$(grep "^POSTGRES_PASSWORD=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 || echo "postgres")
|
||||
|
||||
# 构建 DATABASE_URL (连接到同容器组的 PostgreSQL)
|
||||
DATABASE_URL="postgresql://postgres:$POSTGRES_PASSWORD@127.0.0.1:5432/map_stores?schema=public&connection_limit=20&pool_timeout=60"
|
||||
|
||||
echo "🐘 创建新容器组..."
|
||||
RESULT=$(aliyun eci CreateContainerGroup \
|
||||
--RegionId "$ECI_REGION" \
|
||||
--SecurityGroupId "$ECI_SECURITY_GROUP_ID" \
|
||||
--VSwitchId "$ECI_VSWITCH_ID" \
|
||||
--ContainerGroupName "$ECI_CONTAINER_GROUP_NAME" \
|
||||
--Cpu "$ECI_CPU" \
|
||||
--Memory "$ECI_MEMORY" \
|
||||
--RestartPolicy Always \
|
||||
--EipInstanceId "$ECI_EIP_ID" \
|
||||
--PlainHttpRegistry "$ECI_REGISTRY_INTRANET_URL" \
|
||||
`# NAS 数据卷` \
|
||||
--Volume.1.Name nas-data \
|
||||
--Volume.1.Type NFSVolume \
|
||||
--Volume.1.NFSVolume.Server "$ECI_NAS_SERVER" \
|
||||
--Volume.1.NFSVolume.Path / \
|
||||
`# Container 1: Next.js 应用(从私有 Registry 内网拉取,速度快)` \
|
||||
--Container.1.Name map-collector \
|
||||
--Container.1.Image "$ECI_REGISTRY_INTRANET_URL/d8d-user-release:$IMAGE_TAG" \
|
||||
--Container.1.Cpu 2 \
|
||||
--Container.1.Memory 2 \
|
||||
--Container.1.Port.1.Protocol TCP \
|
||||
--Container.1.Port.1.Port 3001 \
|
||||
--Container.1.ImagePullPolicy Always \
|
||||
--Container.1.EnvironmentVarHide true \
|
||||
--Container.1.EnvironmentVar.1.Key DATABASE_URL \
|
||||
--Container.1.EnvironmentVar.1.Value "$DATABASE_URL" \
|
||||
`# 从 .env 加载 API keys` \
|
||||
--Container.1.EnvironmentVar.2.Key ANTHROPIC_API_KEY \
|
||||
--Container.1.EnvironmentVar.2.Value "$(grep "^ANTHROPIC_API_KEY=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 || echo "")" \
|
||||
--Container.1.EnvironmentVar.3.Key ANTHROPIC_BASE_URL \
|
||||
--Container.1.EnvironmentVar.3.Value "$(grep "^ANTHROPIC_BASE_URL=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 || echo "")" \
|
||||
--Container.1.EnvironmentVar.4.Key ANTHROPIC_MODEL \
|
||||
--Container.1.EnvironmentVar.4.Value "$(grep "^ANTHROPIC_MODEL=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 || echo "")" \
|
||||
--Container.1.EnvironmentVar.5.Key AMAP_API_KEY \
|
||||
--Container.1.EnvironmentVar.5.Value "$(grep "^AMAP_API_KEY=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 || echo "")" \
|
||||
--Container.1.EnvironmentVar.6.Key BAIDU_API_KEY \
|
||||
--Container.1.EnvironmentVar.6.Value "$(grep "^BAIDU_API_KEY=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 || echo "")" \
|
||||
--Container.1.EnvironmentVar.7.Key TENCENT_API_KEY \
|
||||
--Container.1.EnvironmentVar.7.Value "$(grep "^TENCENT_API_KEY=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 || echo "")" \
|
||||
--Container.1.EnvironmentVar.8.Key NEXT_PUBLIC_AMAP_JS_KEY \
|
||||
--Container.1.EnvironmentVar.8.Value "$(grep "^NEXT_PUBLIC_AMAP_JS_KEY=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 || echo "")" \
|
||||
--Container.1.EnvironmentVar.9.Key NEXT_PUBLIC_AMAP_JS_SECRET \
|
||||
--Container.1.EnvironmentVar.9.Value "$(grep "^NEXT_PUBLIC_AMAP_JS_SECRET=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 || echo "")" \
|
||||
`# Container 2: PostgreSQL sidecar(从内网 Registry 拉取预热镜像)` \
|
||||
--Container.2.Name postgres \
|
||||
--Container.2.Image "$ECI_REGISTRY_INTRANET_URL/postgres:17-alpine" \
|
||||
--Container.2.Cpu 0.5 \
|
||||
--Container.2.Memory 1.5 \
|
||||
--Container.2.Port.1.Protocol TCP \
|
||||
--Container.2.Port.1.Port 5432 \
|
||||
--Container.2.EnvironmentVar.1.Key POSTGRES_DB \
|
||||
--Container.2.EnvironmentVar.1.Value map_stores \
|
||||
--Container.2.EnvironmentVar.2.Key POSTGRES_USER \
|
||||
--Container.2.EnvironmentVar.2.Value postgres \
|
||||
--Container.2.EnvironmentVar.3.Key POSTGRES_PASSWORD \
|
||||
--Container.2.EnvironmentVar.3.Value "$POSTGRES_PASSWORD" \
|
||||
--Container.2.VolumeMount.1.Name nas-data \
|
||||
--Container.2.VolumeMount.1.MountPath /var/lib/postgresql/data \
|
||||
--Container.2.VolumeMount.1.ReadOnly false)
|
||||
|
||||
CONTAINER_GROUP_ID=$(echo "$RESULT" | grep -o '"ContainerGroupId":"[^"]*"' | cut -d'"' -f4)
|
||||
echo "✅ 容器组创建成功: $CONTAINER_GROUP_ID"
|
||||
|
||||
echo "⏳ 等待容器组启动..."
|
||||
for i in {1..120}; do
|
||||
STATUS=$(aliyun eci DescribeContainerGroups \
|
||||
--RegionId "$ECI_REGION" \
|
||||
--ContainerGroupId "$CONTAINER_GROUP_ID" 2>/dev/null | grep -o '"Status":"[^"]*"' | cut -d'"' -f4 || echo "Pending")
|
||||
|
||||
if [ "$STATUS" = "Running" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
if [ "$STATUS" = "Failed" ]; then
|
||||
echo "❌ 容器组启动失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " 状态: $STATUS"
|
||||
sleep 3
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "🎉 部署完成!"
|
||||
echo "🌐 访问地址: http://$ECI_EIP_ADDRESS:3001"
|
||||
echo "🔍 健康检查: http://$ECI_EIP_ADDRESS:3001/api/health"
|
||||
echo ""
|
||||
135
.claude/skills/d8d-release/assets/eci-registry.sh
Executable file
135
.claude/skills/d8d-release/assets/eci-registry.sh
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
# ECI 私有 Registry 部署脚本(Spot 实例)
|
||||
# 使用 registry:2 镜像,NAS 持久化存储
|
||||
|
||||
set -e
|
||||
|
||||
# 加载配置
|
||||
source "$(dirname "$0")/../eci.conf"
|
||||
|
||||
REGION=${ECI_REGION:-cn-beijing}
|
||||
CONTAINER_GROUP_NAME="d8d-eci-registry"
|
||||
CPU=0.5
|
||||
MEMORY=1.0
|
||||
|
||||
echo "=== ECI 私有 Registry 部署 ==="
|
||||
echo "容器组名称: $CONTAINER_GROUP_NAME"
|
||||
echo "EIP ID: $ECI_REGISTRY_EIP_ID"
|
||||
echo "EIP 地址: $ECI_REGISTRY_EIP_ADDRESS"
|
||||
echo "NAS 服务器: $ECI_NAS_SERVER"
|
||||
echo ""
|
||||
|
||||
# 检查容器组是否已存在
|
||||
echo "检查容器组是否存在..."
|
||||
EXISTING=$(aliyun eci DescribeContainerGroups --region "$REGION" \
|
||||
--ContainerGroupName "$CONTAINER_GROUP_NAME" 2>/dev/null \
|
||||
| grep -c '"ContainerGroupId"' || true)
|
||||
|
||||
if [ "$EXISTING" -gt 0 ]; then
|
||||
echo "容器组已存在,正在删除..."
|
||||
CONTAINER_GROUP_ID=$(aliyun eci DescribeContainerGroups --region "$REGION" \
|
||||
--ContainerGroupName "$CONTAINER_GROUP_NAME" 2>/dev/null \
|
||||
| grep -o '"ContainerGroupId":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
aliyun eci DeleteContainerGroup --region "$REGION" --ContainerGroupId "$CONTAINER_GROUP_ID" >/dev/null
|
||||
|
||||
# 等待删除完成
|
||||
echo "等待容器组删除..."
|
||||
for i in {1..30}; do
|
||||
STATUS=$(aliyun eci DescribeContainerGroups --region "$REGION" \
|
||||
--ContainerGroupName "$CONTAINER_GROUP_NAME" 2>/dev/null \
|
||||
| grep -o '"Status":"[^"]*"' | cut -d'"' -f4 || true)
|
||||
if [ -z "$STATUS" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "容器组已删除"
|
||||
fi
|
||||
|
||||
# 创建新容器组
|
||||
echo "创建 Registry 容器组(Spot 实例)..."
|
||||
RESULT=$(aliyun eci CreateContainerGroup --region "$REGION" \
|
||||
--ContainerGroupName "$CONTAINER_GROUP_NAME" \
|
||||
--VSwitchId "$ECI_VSWITCH_ID" \
|
||||
--SecurityGroupId "$ECI_SECURITY_GROUP_ID" \
|
||||
--RestartPolicy Always \
|
||||
--Cpu "$CPU" \
|
||||
--Memory "$MEMORY" \
|
||||
--SpotStrategy SpotAsPriceGo \
|
||||
--EipInstanceId "$ECI_REGISTRY_EIP_ID" \
|
||||
--Container.1.Name registry \
|
||||
--Container.1.Image "registry:2" \
|
||||
--Container.1.Cpu "$CPU" \
|
||||
--Container.1.Memory "$MEMORY" \
|
||||
--Container.1.Port.1.Protocol TCP \
|
||||
--Container.1.Port.1.Port 5000 \
|
||||
--Container.1.VolumeMount.1.Name registry-data \
|
||||
--Container.1.VolumeMount.1.MountPath "/var/lib/registry" \
|
||||
--Volume.1.Name registry-data \
|
||||
--Volume.1.Type NFSVolume \
|
||||
--Volume.1.NFSVolume.Server "$ECI_NAS_SERVER" \
|
||||
--Volume.1.NFSVolume.Path "/" \
|
||||
2>&1)
|
||||
|
||||
echo "$RESULT"
|
||||
|
||||
CONTAINER_GROUP_ID=$(echo "$RESULT" | grep -o '"ContainerGroupId":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$CONTAINER_GROUP_ID" ]; then
|
||||
echo "错误:容器组创建失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "容器组 ID: $CONTAINER_GROUP_ID"
|
||||
echo "等待容器组启动..."
|
||||
|
||||
# 等待容器组 Running
|
||||
for i in {1..60}; do
|
||||
STATUS=$(aliyun eci DescribeContainerGroups --region "$REGION" \
|
||||
--ContainerGroupId "$CONTAINER_GROUP_ID" 2>/dev/null \
|
||||
| grep -o '"Status":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
echo "状态: $STATUS"
|
||||
|
||||
if [ "$STATUS" = "Running" ]; then
|
||||
break
|
||||
elif [ "$STATUS" = "Failed" ] || [ "$STATUS" = "Pending" ]; then
|
||||
# 继续等待
|
||||
:
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "获取容器组内网 IP..."
|
||||
for i in {1..30}; do
|
||||
INTRANET_IP=$(aliyun eci DescribeContainerGroups --region "$REGION" \
|
||||
--ContainerGroupId "$CONTAINER_GROUP_ID" 2>/dev/null \
|
||||
| grep -o '"IntranetIp":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ -n "$INTRANET_IP" ]; then
|
||||
echo "内网 IP: $INTRANET_IP"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# 保存内网 IP 到 eci.conf
|
||||
ECI_CONF="$(dirname "$0")/../eci.conf"
|
||||
sed -i "/^ECI_REGISTRY_INTRANET_IP=/d" "$ECI_CONF"
|
||||
sed -i "/^ECI_REGISTRY_INTRANET_URL=/d" "$ECI_CONF"
|
||||
echo "" >> "$ECI_CONF"
|
||||
echo "# 私有 Registry 内网 IP(用于应用 ECI 内网拉取镜像)" >> "$ECI_CONF"
|
||||
echo "ECI_REGISTRY_INTRANET_IP=$INTRANET_IP" >> "$ECI_CONF"
|
||||
echo "ECI_REGISTRY_INTRANET_URL=\${ECI_REGISTRY_INTRANET_IP}:5000" >> "$ECI_CONF"
|
||||
echo "已保存内网 IP 到 eci.conf"
|
||||
|
||||
echo ""
|
||||
echo "=== 部署完成 ==="
|
||||
echo "私有 Registry 外网地址: $ECI_REGISTRY_EIP_ADDRESS:$ECI_REGISTRY_PORT"
|
||||
echo "私有 Registry 内网地址: $INTRANET_IP:5000"
|
||||
echo "外网测试: curl http://$ECI_REGISTRY_EIP_ADDRESS:$ECI_REGISTRY_PORT/v2/"
|
||||
echo "内网测试: curl http://$INTRANET_IP:5000/v2/ (需在 VPC 内)"
|
||||
echo ""
|
||||
41
.claude/skills/d8d-release/assets/eci.conf.template
Normal file
41
.claude/skills/d8d-release/assets/eci.conf.template
Normal file
@@ -0,0 +1,41 @@
|
||||
# ECI 部署配置
|
||||
# 由 d8d-release 技能自动填充,请勿手动修改
|
||||
|
||||
# 阿里云区域
|
||||
ECI_REGION=cn-beijing
|
||||
|
||||
# 基础设施资源 ID(由技能自动创建并填充)
|
||||
ECI_VPC_ID=
|
||||
ECI_VSWITCH_ID=
|
||||
ECI_SECURITY_GROUP_ID=
|
||||
ECI_NAS_SERVER=
|
||||
|
||||
# 应用 EIP
|
||||
ECI_EIP_ID=
|
||||
ECI_EIP_ADDRESS=
|
||||
|
||||
# 私有 Registry EIP 和地址(ECI Spot 实例)
|
||||
ECI_REGISTRY_EIP_ID=
|
||||
ECI_REGISTRY_EIP_ADDRESS=
|
||||
ECI_REGISTRY_PORT=5000
|
||||
ECI_REGISTRY_URL=
|
||||
|
||||
# 私有 Registry 内网 IP(用于应用 ECI 内网拉取镜像,由 eci-registry.sh 自动填充)
|
||||
ECI_REGISTRY_INTRANET_IP=
|
||||
ECI_REGISTRY_INTRANET_URL=
|
||||
|
||||
# RAM AK/SK(用于 ECI 部署)
|
||||
ALI_ACCESS_KEY_ID=
|
||||
ALI_ACCESS_KEY_SECRET=
|
||||
|
||||
# Gitea 配置(用于 Actions 日志查询)
|
||||
GITEA_URL=https://gitea.d8d.fun
|
||||
GITEA_USERNAME=d8dfun
|
||||
GITEA_PASSWORD=zyh81569851
|
||||
GITEA_REPO_OWNER=d8dfun
|
||||
GITEA_REPO_NAME=
|
||||
|
||||
# ECI 容器组规格
|
||||
ECI_CONTAINER_GROUP_NAME=map-collector
|
||||
ECI_CPU=2.5
|
||||
ECI_MEMORY=3.5
|
||||
240
.claude/skills/d8d-release/assets/gitea-log.py
Normal file
240
.claude/skills/d8d-release/assets/gitea-log.py
Normal file
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Gitea Actions 构建日志查询
|
||||
|
||||
通过 Gitea API 查询指定 run ID 的 Actions 构建日志。
|
||||
|
||||
使用方法:
|
||||
python scripts/gitea-log.py <run_id> [job_id]
|
||||
"""
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import re
|
||||
from urllib.parse import urljoin
|
||||
|
||||
# 从 gitea.conf 读取 Gitea 配置(仅本地开发用,不提交)
|
||||
def load_gitea_config():
|
||||
import os
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
config_path = os.path.join(script_dir, "..", "gitea.conf")
|
||||
|
||||
config = {}
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
config[key.strip()] = value.strip()
|
||||
|
||||
return config
|
||||
|
||||
CONFIG = load_gitea_config()
|
||||
|
||||
GITEA_URL = CONFIG.get("GITEA_URL", "https://gitea.d8d.fun")
|
||||
GITEA_USERNAME = CONFIG.get("GITEA_USERNAME", "")
|
||||
GITEA_PASSWORD = CONFIG.get("GITEA_PASSWORD", "")
|
||||
REPO_OWNER = CONFIG.get("GITEA_REPO_OWNER", "d8dfun")
|
||||
REPO_NAME = CONFIG.get("GITEA_REPO_NAME", "")
|
||||
|
||||
|
||||
def get_gitea_actions_log(run_id, job_id="0"):
|
||||
"""
|
||||
获取 Gitea Actions 构建日志
|
||||
"""
|
||||
session = requests.Session()
|
||||
csrf_token = None
|
||||
|
||||
print(f"🔐 正在登录 Gitea...")
|
||||
|
||||
login_url = urljoin(GITEA_URL, "/user/login")
|
||||
response = session.get(login_url)
|
||||
|
||||
csrf_match = re.search(r'<input type="hidden" name="_csrf" value="([^"]+)"', response.text, re.IGNORECASE)
|
||||
if not csrf_match:
|
||||
csrf_match = re.search(r"csrfToken: '([^']+)'", response.text)
|
||||
if not csrf_match:
|
||||
csrf_match = re.search(r'<meta name="_csrf" content="([^"]+)"', response.text)
|
||||
|
||||
if csrf_match:
|
||||
csrf_token = csrf_match.group(1)
|
||||
else:
|
||||
return {"error": "无法获取 CSRF token"}
|
||||
|
||||
login_data = {
|
||||
"user_name": GITEA_USERNAME,
|
||||
"password": GITEA_PASSWORD,
|
||||
"_csrf": csrf_token
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Csrf-Token": csrf_token
|
||||
}
|
||||
|
||||
response = session.post(login_url, data=login_data, headers=headers, allow_redirects=True)
|
||||
|
||||
if response.status_code != 200 or "login" in response.url:
|
||||
return {"error": "登录失败"}
|
||||
|
||||
for cookie in session.cookies:
|
||||
if cookie.name == "_csrf":
|
||||
csrf_token = cookie.value
|
||||
break
|
||||
|
||||
print(f"✓ 登录成功")
|
||||
|
||||
url = f"{GITEA_URL}/{REPO_OWNER}/{REPO_NAME}/actions/runs/{run_id}/jobs/{job_id}"
|
||||
|
||||
print(f"\n📋 正在获取构建日志...")
|
||||
print(f" URL: {url}")
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Csrf-Token": csrf_token
|
||||
}
|
||||
|
||||
response = session.post(url, json={"logCursors": []}, headers=headers)
|
||||
|
||||
if response.status_code != 200:
|
||||
return {"error": f"HTTP {response.status_code}", "message": response.text}
|
||||
|
||||
data = response.json()
|
||||
|
||||
steps = data.get("state", {}).get("currentJob", {}).get("steps", [])
|
||||
|
||||
print(f" 发现 {len(steps)} 个步骤")
|
||||
|
||||
all_logs = []
|
||||
for step_idx in range(len(steps)):
|
||||
log_cursors = [{
|
||||
"step": step_idx,
|
||||
"cursor": None,
|
||||
"expanded": True
|
||||
}]
|
||||
|
||||
response = session.post(url, json={"logCursors": log_cursors}, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
steps_log = result.get("logs", {}).get("stepsLog", [])
|
||||
|
||||
if steps_log:
|
||||
for step_log in steps_log:
|
||||
if step_log.get("step") == step_idx:
|
||||
log_entries = step_log.get("content") or step_log.get("lines") or []
|
||||
all_logs.append({
|
||||
"step": step_idx,
|
||||
"summary": steps[step_idx].get("summary", ""),
|
||||
"status": steps[step_idx].get("status", ""),
|
||||
"logs": log_entries
|
||||
})
|
||||
print(f" ✓ 步骤 {step_idx}: {len(log_entries)} 行日志")
|
||||
break
|
||||
|
||||
result = {
|
||||
"run": data.get("state", {}).get("run", {}),
|
||||
"job": data.get("state", {}).get("currentJob", {}),
|
||||
"logs": all_logs
|
||||
}
|
||||
|
||||
return format_build_log(result)
|
||||
|
||||
|
||||
def format_build_log(data):
|
||||
"""格式化构建日志输出"""
|
||||
run_info = data.get("run", {})
|
||||
job_info = data.get("job", {})
|
||||
logs = data.get("logs", [])
|
||||
|
||||
lines = []
|
||||
lines.append("=" * 80)
|
||||
lines.append(f"📦 运行: {run_info.get('title', 'N/A')}")
|
||||
lines.append(f"📊 状态: {run_info.get('status', 'N/A')}")
|
||||
lines.append(f"📝 任务: {job_info.get('title', 'N/A')}")
|
||||
lines.append(f"📄 详情: {job_info.get('detail', 'N/A')}")
|
||||
|
||||
commit = run_info.get("commit", {})
|
||||
if commit:
|
||||
lines.append(f"🔀 提交: {commit.get('shortSHA', 'N/A')} - {commit.get('pusher', {}).get('displayName', 'N/A')}")
|
||||
lines.append(f"🌿 分支: {commit.get('branch', {}).get('name', 'N/A')}")
|
||||
|
||||
lines.append("=" * 80)
|
||||
lines.append("")
|
||||
|
||||
steps = job_info.get("steps", [])
|
||||
if steps:
|
||||
lines.append("📋 步骤执行情况:")
|
||||
for idx, step in enumerate(steps):
|
||||
summary = step.get("summary", "N/A")
|
||||
duration = step.get("duration", "N/A")
|
||||
status = step.get("status", "")
|
||||
|
||||
status_emoji = {
|
||||
"success": "✅",
|
||||
"failure": "❌",
|
||||
"skipped": "⏭️",
|
||||
"running": "⏳"
|
||||
}.get(status, "❓")
|
||||
|
||||
lines.append(f" [{idx}] {status_emoji} {summary} ({duration})")
|
||||
|
||||
lines.append("")
|
||||
lines.append("-" * 80)
|
||||
lines.append("📝 构建日志:")
|
||||
lines.append("")
|
||||
|
||||
if logs:
|
||||
for log_item in logs:
|
||||
step_num = log_item.get("step", "?")
|
||||
summary = log_item.get("summary", "")
|
||||
status = log_item.get("status", "")
|
||||
log_entries = log_item.get("logs", [])
|
||||
|
||||
status_emoji = {
|
||||
"success": "✅",
|
||||
"failure": "❌",
|
||||
"skipped": "⏭️",
|
||||
"running": "⏳"
|
||||
}.get(status, "❓")
|
||||
|
||||
lines.append(f"{status_emoji} 步骤 {step_num}: {summary}")
|
||||
lines.append("")
|
||||
|
||||
if log_entries:
|
||||
for entry in log_entries[:200]:
|
||||
if isinstance(entry, dict):
|
||||
message = entry.get("message", str(entry))
|
||||
timestamp = entry.get("timestamp", "")
|
||||
if timestamp:
|
||||
lines.append(f" [{timestamp}] {message}")
|
||||
else:
|
||||
lines.append(f" {message}")
|
||||
else:
|
||||
lines.append(f" {entry}")
|
||||
|
||||
if len(log_entries) > 200:
|
||||
lines.append(f" ... 还有 {len(log_entries) - 200} 行日志")
|
||||
lines.append("")
|
||||
else:
|
||||
lines.append("⚠️ 没有找到日志内容")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: python scripts/gitea-log.py <run_id> [job_id]")
|
||||
sys.exit(1)
|
||||
|
||||
run_id = sys.argv[1]
|
||||
job_id = sys.argv[2] if len(sys.argv) > 2 else "0"
|
||||
|
||||
result = get_gitea_actions_log(run_id, job_id)
|
||||
|
||||
if isinstance(result, dict) and "error" in result:
|
||||
print(f"❌ 错误: {result['error']}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(result)
|
||||
8
.claude/skills/d8d-release/assets/gitea.conf.template
Normal file
8
.claude/skills/d8d-release/assets/gitea.conf.template
Normal file
@@ -0,0 +1,8 @@
|
||||
# Gitea 配置(仅本地开发环境使用,不提交到仓库)
|
||||
# 用于 Gitea Actions 构建日志查询
|
||||
|
||||
GITEA_URL=https://gitea.d8d.fun
|
||||
GITEA_USERNAME=d8dfun
|
||||
GITEA_PASSWORD=zyh81569851
|
||||
GITEA_REPO_OWNER=d8dfun
|
||||
GITEA_REPO_NAME=
|
||||
93
.claude/skills/d8d-release/assets/release.yaml
Normal file
93
.claude/skills/d8d-release/assets/release.yaml
Normal file
@@ -0,0 +1,93 @@
|
||||
name: Docker Build and Push
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "🎉 该作业由 ${{ gitea.event_name }} 事件自动触发。"
|
||||
- run: echo "🐧 此作业当前在 Gitea 托管的 ${{ runner.os }} 服务器上运行!"
|
||||
- run: echo "🔎 您的标签名称是 ${{ gitea.ref_name }},仓库是 ${{ gitea.repository }}。"
|
||||
|
||||
- name: 检出仓库代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- run: echo "💡 ${{ gitea.repository }} 仓库已克隆到运行器。"
|
||||
- run: echo "🖥️ 工作流现在已准备好在运行器上测试您的代码。"
|
||||
|
||||
- name: 列出仓库中的文件
|
||||
run: |
|
||||
ls ${{ gitea.workspace }}
|
||||
|
||||
- run: echo "🍏 此作业的状态是 ${{ job.status }}。"
|
||||
|
||||
- name: 提取版本号和处理仓库名
|
||||
id: extract_info
|
||||
run: |
|
||||
# 从标签名中提取版本号(例如从 release/v0.1.6 中提取 v0.1.6)
|
||||
VERSION=$(echo "${{ gitea.ref_name }}" | sed 's|release/||')
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
# 处理仓库名(替换斜杠为连字符,避免Docker标签错误)
|
||||
REPO_NAME=$(echo "${{ gitea.repository }}" | sed 's|/|-|g')
|
||||
echo "REPO_NAME=$REPO_NAME" >> $GITHUB_ENV
|
||||
|
||||
# 从 eci.conf 加载 Registry 地址
|
||||
source eci.conf
|
||||
echo "REGISTRY_URL=$ECI_REGISTRY_URL" >> $GITHUB_ENV
|
||||
|
||||
echo "提取的版本号:$VERSION"
|
||||
echo "处理后的仓库名:$REPO_NAME"
|
||||
echo "Registry 地址:$ECI_REGISTRY_URL"
|
||||
|
||||
- name: 设置 Docker Buildx(支持 HTTP 私有 Registry)
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:master
|
||||
network=host
|
||||
buildkitd-config-inline: |
|
||||
[registry."${{ env.REGISTRY_URL }}"]
|
||||
http = true
|
||||
insecure = true
|
||||
|
||||
- name: 构建并推送应用镜像到私有 Registry
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY_URL }}/d8d-user-release:${{ env.REPO_NAME }}-${{ env.VERSION }}
|
||||
|
||||
- name: 预热 postgres:17-alpine 镜像到私有 Registry
|
||||
run: |
|
||||
source eci.conf
|
||||
# 从 Docker Hub 拉取官方 postgres:17-alpine
|
||||
docker pull postgres:17-alpine
|
||||
# 重命名为私有 Registry 地址
|
||||
docker tag postgres:17-alpine $ECI_REGISTRY_URL/postgres:17-alpine
|
||||
# 推送到私有 Registry
|
||||
docker push $ECI_REGISTRY_URL/postgres:17-alpine
|
||||
echo "postgres:17-alpine 镜像预热完成"
|
||||
|
||||
- name: 安装 aliyun CLI(用于 ECI 部署)
|
||||
run: |
|
||||
curl -o aliyun-cli.tgz https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz
|
||||
tar -xzf aliyun-cli.tgz
|
||||
chmod +x aliyun
|
||||
mv aliyun /usr/local/bin/
|
||||
|
||||
- name: 配置阿里云凭证(从 eci.conf 读取)
|
||||
run: |
|
||||
source eci.conf
|
||||
aliyun configure set --mode AK --access-key-id "$ALI_ACCESS_KEY_ID" --access-key-secret "$ALI_ACCESS_KEY_SECRET" --region cn-beijing
|
||||
|
||||
- name: 部署到 ECI
|
||||
run: |
|
||||
chmod +x scripts/eci-deploy.sh
|
||||
IMAGE_TAG="${{ env.REPO_NAME }}-${{ env.VERSION }}"
|
||||
./scripts/eci-deploy.sh "$IMAGE_TAG"
|
||||
88
.claude/skills/d8d-release/assets/release_tag.sh
Normal file
88
.claude/skills/d8d-release/assets/release_tag.sh
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 发布脚本
|
||||
# 使用方法: ./release.sh <版本号>
|
||||
# 例如: ./release.sh 1.0.0
|
||||
# 查看标签: ./release.sh --list 或 ./release.sh -l
|
||||
|
||||
set -e
|
||||
|
||||
# 检查参数
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "错误: 请提供版本号"
|
||||
echo "使用方法: $0 <版本号>"
|
||||
echo "例如: $0 1.0.0"
|
||||
echo ""
|
||||
echo "📋 Gitea 最近10个标签:"
|
||||
# 尝试获取远程标签,如果失败则使用本地标签
|
||||
REMOTE_TAGS=$(git ls-remote --tags __REMOTE__ 2>/dev/null | grep -v "\^{}" | cut -f 2 | sed 's|refs/tags/||')
|
||||
if [ -z "$REMOTE_TAGS" ]; then
|
||||
echo "⚠️ 无法获取远程标签,显示本地最近10个标签:"
|
||||
git tag --list | sort -V | tail -10
|
||||
else
|
||||
echo "$REMOTE_TAGS" | sort -V | uniq | tail -10
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 特殊参数: 查看所有标签
|
||||
if [ "$1" = "--list" ] || [ "$1" = "-l" ]; then
|
||||
echo "📋 Gitea 所有标签:"
|
||||
# 尝试获取远程标签,如果失败则使用本地标签
|
||||
REMOTE_TAGS=$(git ls-remote --tags __REMOTE__ 2>/dev/null | grep -v "\^{}" | cut -f 2 | sed 's|refs/tags/||')
|
||||
if [ -z "$REMOTE_TAGS" ]; then
|
||||
echo "⚠️ 无法获取远程标签,显示本地标签:"
|
||||
git tag --list | sort -V
|
||||
else
|
||||
echo "$REMOTE_TAGS" | sort -V | uniq
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
VERSION="$1"
|
||||
TAG="v$VERSION"
|
||||
|
||||
# 验证版本号格式
|
||||
if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "错误: 版本号格式不正确,请使用语义化版本格式 (例如: 1.0.0)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🚀 开始发布 版本: $VERSION"
|
||||
|
||||
# 检查工作目录是否干净
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "⚠️ 警告: 工作目录有未提交的更改"
|
||||
git status
|
||||
read -p "是否继续发布? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "发布已取消"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查当前分支
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
echo "🌿 当前分支: $CURRENT_BRANCH"
|
||||
|
||||
# 拉取最新代码
|
||||
echo "📥 拉取最新代码..."
|
||||
git pull __REMOTE__ "$CURRENT_BRANCH"
|
||||
|
||||
|
||||
# 创建标签
|
||||
echo "🏷️ 创建标签 $TAG..."
|
||||
git tag "$TAG"
|
||||
|
||||
# 推送标签
|
||||
echo "📤 推送标签到远程仓库..."
|
||||
git push __REMOTE__ "$TAG"
|
||||
|
||||
echo ""
|
||||
echo "🎉 发布流程已启动!"
|
||||
echo "📋 版本: $VERSION"
|
||||
echo "🏷️ 标签: $TAG"
|
||||
echo "🔗 GitHub Actions 将自动构建和发布 Docker 镜像"
|
||||
echo ""
|
||||
echo "📊 查看构建进度: $(git config --get remote.__REMOTE__.url | sed 's|^ssh://||' | sed 's|^git@||' | sed 's|:|/|' | sed 's|\.git$||')/actions"
|
||||
Reference in New Issue
Block a user