ai-engineering-init 1.17.0 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/skill-forced-eval.js +1 -0
- package/.claude/skills/jenkins-deploy/SKILL.md +207 -0
- package/.claude/skills/jenkins-deploy/assets/env_param.template.json +51 -0
- package/.claude/skills/jenkins-deploy/assets/jk_build.py +362 -0
- package/.claude/skills/jenkins-deploy/assets/last_cd_env.template.json +7 -0
- package/.codex/skills/jenkins-deploy/SKILL.md +207 -0
- package/.cursor/hooks/cursor-skill-eval.js +4 -0
- package/.cursor/rules/skill-activation.mdc +1 -0
- package/.cursor/skills/jenkins-deploy/SKILL.md +207 -0
- package/AGENTS.md +1 -0
- package/bin/index.js +19 -0
- package/package.json +1 -1
|
@@ -117,6 +117,7 @@ const instructions = `## 强制技能激活流程(必须执行)
|
|
|
117
117
|
- skill-creator: 创建技能模板/技能脚手架/skill scaffold
|
|
118
118
|
- leniu-report-scenario: 报表/报表开发/报表查询/报表导出/合计行/totalLine/汇总报表/定制报表/report_order_info/金额处理/分转元/餐次/mealtime/ReportBaseTotalVO/CustomNumberConverter
|
|
119
119
|
- leniu-marketing-scenario: 营销/营销规则/消费规则/计价规则/充值规则/扣款规则/就餐规则/折扣/满减/限额/限次/补贴/赠送/满赠/管理费/MarketApi/RulePriceHandler/RuleRechargeHandler/RulePayHandler/MarketRule/规则定制
|
|
120
|
+
- jenkins-deploy: 打包/部署/Jenkins/构建/Portainer/发布到dev/发布到test/更新环境/自动部署
|
|
120
121
|
|
|
121
122
|
### 步骤 2 - 激活(逐个调用,等待每个完成)
|
|
122
123
|
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jenkins-deploy
|
|
3
|
+
description: |
|
|
4
|
+
Jenkins + Portainer 自动打包部署技能。通过 Python 脚本调用 Jenkins API 构建项目,并触发 Portainer Webhook/Update 完成容器更新。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- 需要将代码打包部署到 dev/test 环境
|
|
8
|
+
- 需要触发 Jenkins 构建 core 或 api 项目
|
|
9
|
+
- 需要更新 Portainer 容器服务
|
|
10
|
+
- 需要查看或修改构建配置(分支、环境、模式)
|
|
11
|
+
- 定制项目的打包部署
|
|
12
|
+
|
|
13
|
+
触发词:打包、部署、Jenkins、构建、Portainer、发布到dev、发布到test、更新环境、自动部署
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Jenkins + Portainer 自动打包部署
|
|
17
|
+
|
|
18
|
+
## 概述
|
|
19
|
+
|
|
20
|
+
项目使用 `jenkins/` 目录下的 Python 脚本实现自动化构建部署,流程:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Jenkins 构建 core → Jenkins 构建 api → Portainer 更新容器
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 文件位置
|
|
27
|
+
|
|
28
|
+
| 文件 | 作用 |
|
|
29
|
+
|------|------|
|
|
30
|
+
| `jenkins/jk_build.py` | 主构建脚本 |
|
|
31
|
+
| `jenkins/env_param.json` | 环境配置(Jenkins/Portainer 连接参数) |
|
|
32
|
+
| `jenkins/last_cd_env.json` | 上次构建参数(自动保存) |
|
|
33
|
+
|
|
34
|
+
## 构建模式
|
|
35
|
+
|
|
36
|
+
| 模式 | 说明 | 执行步骤 |
|
|
37
|
+
|------|------|---------|
|
|
38
|
+
| `0` | 只构建 | 构建 core + api,不更新 Portainer |
|
|
39
|
+
| `1` | 全构建+更新 | 构建 core + api → 触发 Portainer 更新 |
|
|
40
|
+
| `2` | 构建 api+更新 | 跳过 core,构建 api → 触发 Portainer 更新 |
|
|
41
|
+
| `3` | 只更新 | 不构建,直接触发 Portainer 更新 |
|
|
42
|
+
|
|
43
|
+
## 环境支持
|
|
44
|
+
|
|
45
|
+
| 环境 | 前缀 | Jenkins Job | Portainer |
|
|
46
|
+
|------|------|-------------|-----------|
|
|
47
|
+
| 开发环境 | `dev` + 编号 | `dev-tengyun-core` / `dev-tengyun-yunshitang-api` | devops-dev.xnzn.net |
|
|
48
|
+
| 测试环境 | `test` + 编号 | `test-tengyun-core` / `test-tengyun-yunshitang-api` | devops-test.xnzn.net |
|
|
49
|
+
| dev16+ | `dev16` ~ `dev43` | 同 dev | xnzn-dev.xnzn.net(使用 forceupdateservice) |
|
|
50
|
+
| dev44+ | `dev44` 及以上 | 只支持模式 0 | 需手动更新 |
|
|
51
|
+
|
|
52
|
+
## 定制项目支持
|
|
53
|
+
|
|
54
|
+
当指定 `api_param_folder`(定制工程文件夹名)时,Jenkins Job 路径变为:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
{folder_name}/dev-后端-core # 替代 dev-tengyun-core
|
|
58
|
+
{folder_name}/dev-后端-api # 替代 dev-tengyun-yunshitang-api
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 使用方式
|
|
62
|
+
|
|
63
|
+
### 直接运行脚本
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd jenkins && python jk_build.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
脚本会交互式询问:
|
|
70
|
+
1. **模式**(0/1/2/3)
|
|
71
|
+
2. **环境**(dev1, dev2, test1 等)
|
|
72
|
+
3. **core 分支**(如 release_5.56.0)
|
|
73
|
+
4. **api 分支**(如 master)
|
|
74
|
+
5. **定制工程文件夹**(可选,空格或 None 跳过)
|
|
75
|
+
|
|
76
|
+
### 通过 Claude 辅助部署
|
|
77
|
+
|
|
78
|
+
当用户说"打包到 devX"时:
|
|
79
|
+
|
|
80
|
+
1. **读取** `jenkins/last_cd_env.json` 获取上次构建参数
|
|
81
|
+
2. **确认参数**:环境、分支、模式、是否定制项目
|
|
82
|
+
3. **修改** `last_cd_env.json` 写入新参数
|
|
83
|
+
4. **执行** `cd jenkins && python jk_build.py`(脚本读取预设值,用户直接回车即可)
|
|
84
|
+
|
|
85
|
+
## 配置文件说明
|
|
86
|
+
|
|
87
|
+
### last_cd_env.json(构建参数)
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"build_mode": "0",
|
|
92
|
+
"cd_env": "dev63",
|
|
93
|
+
"core_param_branch": "release_5.56.0",
|
|
94
|
+
"api_param_branch": "master",
|
|
95
|
+
"api_param_folder": null
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| 字段 | 说明 |
|
|
100
|
+
|------|------|
|
|
101
|
+
| `build_mode` | 构建模式(0/1/2/3) |
|
|
102
|
+
| `cd_env` | 目标环境(如 dev1, test2) |
|
|
103
|
+
| `core_param_branch` | core 仓库分支 |
|
|
104
|
+
| `api_param_branch` | api 仓库分支 |
|
|
105
|
+
| `api_param_folder` | 定制项目文件夹(null 表示标准项目) |
|
|
106
|
+
|
|
107
|
+
### env_param.json(环境连接配置)
|
|
108
|
+
|
|
109
|
+
包含 Jenkins 和 Portainer 的连接参数(用户名、API Token、服务地址等),按 dev/test 环境分组。
|
|
110
|
+
|
|
111
|
+
> **注意**:此文件包含敏感凭证,不要将内容输出到对话中。
|
|
112
|
+
|
|
113
|
+
## 构建流程详解
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
1. 连接 Jenkins(ci.xnzn.net)
|
|
117
|
+
2. 构建 core(参数:BRANCH + VERSION=环境名)
|
|
118
|
+
└─ 轮询构建进度,超时 10 分钟
|
|
119
|
+
3. 构建 api(参数同上)
|
|
120
|
+
└─ 轮询构建进度,超时 5 分钟
|
|
121
|
+
4. 更新 Portainer:
|
|
122
|
+
├─ dev1~15:Webhook 触发
|
|
123
|
+
│ └─ 获取 JWT → 查 Service ID → 获取/创建 Webhook → POST 触发
|
|
124
|
+
└─ dev16+:Force Update
|
|
125
|
+
└─ 获取 JWT → 查 Service ID → PUT forceupdateservice
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## 常见操作
|
|
129
|
+
|
|
130
|
+
### 只改了 api 代码,快速部署
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# 修改 last_cd_env.json 的 build_mode 为 "2",然后运行
|
|
134
|
+
cd jenkins && python jk_build.py
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 只需要更新容器(已在 Jenkins 手动构建完)
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# build_mode 设为 "3"
|
|
141
|
+
cd jenkins && python jk_build.py
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 部署定制项目
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
// last_cd_env.json
|
|
148
|
+
{
|
|
149
|
+
"api_param_folder": "leniu-tengyun-wuhanxieheyiyuan"
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 首次初始化(团队成员)
|
|
154
|
+
|
|
155
|
+
当用户说"初始化部署环境"或检测到 `jenkins/` 目录不存在时,执行以下步骤:
|
|
156
|
+
|
|
157
|
+
### 自动初始化流程
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# 1. 创建 jenkins 目录
|
|
161
|
+
mkdir -p jenkins
|
|
162
|
+
|
|
163
|
+
# 2. 从技能模板复制文件(SKILL_DIR 为技能目录路径)
|
|
164
|
+
cp .claude/skills/jenkins-deploy/assets/jk_build.py jenkins/jk_build.py
|
|
165
|
+
cp .claude/skills/jenkins-deploy/assets/last_cd_env.template.json jenkins/last_cd_env.json
|
|
166
|
+
|
|
167
|
+
# 3. 环境配置需要用户提供(含敏感凭证)
|
|
168
|
+
cp .claude/skills/jenkins-deploy/assets/env_param.template.json jenkins/env_param.json
|
|
169
|
+
|
|
170
|
+
# 4. 安装依赖
|
|
171
|
+
pip install python-jenkins requests
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 配置凭证
|
|
175
|
+
|
|
176
|
+
初始化后,`jenkins/env_param.json` 中的占位符需要替换为真实值:
|
|
177
|
+
|
|
178
|
+
| 占位符 | 说明 | 获取方式 |
|
|
179
|
+
|--------|------|---------|
|
|
180
|
+
| `__JENKINS_DEV_USER__` | Jenkins dev 账号 | 向团队负责人获取 |
|
|
181
|
+
| `__JENKINS_DEV_TOKEN__` | Jenkins dev API Token | Jenkins → 用户 → 设置 → API Token |
|
|
182
|
+
| `__JENKINS_TEST_USER__` | Jenkins test 账号 | 同上 |
|
|
183
|
+
| `__JENKINS_TEST_TOKEN__` | Jenkins test API Token | 同上 |
|
|
184
|
+
| `__PORTAINER_*_USER__` | Portainer 用户名 | 向团队负责人获取 |
|
|
185
|
+
| `__PORTAINER_*_PWD__` | Portainer 密码 | 向团队负责人获取 |
|
|
186
|
+
|
|
187
|
+
**快捷方式**:如果团队已有成员配置好,可直接拷贝对方的 `env_param.json` 文件。
|
|
188
|
+
|
|
189
|
+
### AI 初始化行为
|
|
190
|
+
|
|
191
|
+
当技能被触发但 `jenkins/` 不存在时:
|
|
192
|
+
1. 提示用户"检测到尚未初始化部署环境"
|
|
193
|
+
2. 询问是否执行初始化
|
|
194
|
+
3. 执行上述复制步骤
|
|
195
|
+
4. 提示用户填写凭证(或拷贝已有配置)
|
|
196
|
+
|
|
197
|
+
## 依赖
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pip install python-jenkins requests
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## 注意
|
|
204
|
+
|
|
205
|
+
- 本技能用于 dev/test 环境部署,**不涉及生产环境**
|
|
206
|
+
- 如果是 Git 提交/分支管理,请使用 `git-workflow` 技能
|
|
207
|
+
- 如果是代码构建错误排查,请使用 `bug-detective` 技能
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"portainer_api_url": {
|
|
3
|
+
"dev": "https://devops-dev.xnzn.net/api",
|
|
4
|
+
"dev16": "https://xnzn-dev.xnzn.net/api",
|
|
5
|
+
"test": "https://devops-test.xnzn.net/api"
|
|
6
|
+
},
|
|
7
|
+
"portainer_user_name": {
|
|
8
|
+
"dev": "__PORTAINER_DEV_USER__",
|
|
9
|
+
"dev16": "__PORTAINER_DEV16_USER__",
|
|
10
|
+
"test": "__PORTAINER_TEST_USER__"
|
|
11
|
+
},
|
|
12
|
+
"portainer_user_pwd": {
|
|
13
|
+
"dev": "__PORTAINER_DEV_PWD__",
|
|
14
|
+
"dev16": "__PORTAINER_DEV16_PWD__",
|
|
15
|
+
"test": "__PORTAINER_TEST_PWD__"
|
|
16
|
+
},
|
|
17
|
+
"portainer_endpoints": {
|
|
18
|
+
"dev": "2",
|
|
19
|
+
"dev16": "1",
|
|
20
|
+
"test": "2"
|
|
21
|
+
},
|
|
22
|
+
"portainer_server_suffix": {
|
|
23
|
+
"dev": "_yunshitang-api",
|
|
24
|
+
"dev16": "_yunshitang-api-offline",
|
|
25
|
+
"test": "_yunshitang-api"
|
|
26
|
+
},
|
|
27
|
+
"core_job": {
|
|
28
|
+
"dev": "dev-tengyun-core",
|
|
29
|
+
"test": "test-tengyun-core"
|
|
30
|
+
},
|
|
31
|
+
"core_job_custom": {
|
|
32
|
+
"dev": "dev-后端-core",
|
|
33
|
+
"test": "test-后端-core"
|
|
34
|
+
},
|
|
35
|
+
"api_job": {
|
|
36
|
+
"dev": "dev-tengyun-yunshitang-api",
|
|
37
|
+
"test": "test-tengyun-yunshitang-api"
|
|
38
|
+
},
|
|
39
|
+
"api_job_custom": {
|
|
40
|
+
"dev": "dev-后端-api",
|
|
41
|
+
"test": "test-后端-api"
|
|
42
|
+
},
|
|
43
|
+
"jenkins_user_id": {
|
|
44
|
+
"dev": "__JENKINS_DEV_USER__",
|
|
45
|
+
"test": "__JENKINS_TEST_USER__"
|
|
46
|
+
},
|
|
47
|
+
"jenkins_api_token": {
|
|
48
|
+
"dev": "__JENKINS_DEV_TOKEN__",
|
|
49
|
+
"test": "__JENKINS_TEST_TOKEN__"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# 简单调用jenkins api,构建项目并触发webhook
|
|
2
|
+
import html
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import jenkins
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
# 预设默认值(可修改)
|
|
11
|
+
cd_env = '' # dev或test
|
|
12
|
+
|
|
13
|
+
# dev和test环境独立配置
|
|
14
|
+
jenkins_server_url = 'https://ci.xnzn.net' # jenkins地址
|
|
15
|
+
portainer_server_suffix = '' # portainer服务名后缀
|
|
16
|
+
core_timeout = 60 * 10 # core构建超时时间
|
|
17
|
+
api_timeout = 60 * 5 # api构建超时时间
|
|
18
|
+
portainer_user_name = '' # portainer用户名
|
|
19
|
+
portainer_user_pwd = '' # portainer密码
|
|
20
|
+
portainer_api_url = ''
|
|
21
|
+
portainer_endpoints = ''
|
|
22
|
+
jenkins_user_id = ''
|
|
23
|
+
jenkins_api_token = ''
|
|
24
|
+
core_job = ''
|
|
25
|
+
api_job = ''
|
|
26
|
+
|
|
27
|
+
# 执行job方法,通过等待时间来判断是否构建成功
|
|
28
|
+
def do_job_by_time(server, job_name, param_branch, param_env, wait_time):
|
|
29
|
+
queue_num = server.build_job(name=job_name,
|
|
30
|
+
parameters={'BRANCH': param_branch, 'VERSION': param_env},
|
|
31
|
+
token=jenkins_api_token)
|
|
32
|
+
print('创建queue... {queue_num}')
|
|
33
|
+
time.sleep(wait_time)
|
|
34
|
+
queue_info = server.get_queue_item(queue_num)
|
|
35
|
+
|
|
36
|
+
print('等待时间到,queue信息:' + str(queue_info))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# 执行job方法,通过请求判断结束,进度更准确
|
|
40
|
+
def do_job(server, job_name, param_branch, param_env, wait_time):
|
|
41
|
+
|
|
42
|
+
queue_num = server.build_job(name=job_name,
|
|
43
|
+
parameters={'BRANCH': param_branch, 'VERSION': param_env},
|
|
44
|
+
token=jenkins_api_token)
|
|
45
|
+
print(f'创建队列... {queue_num}')
|
|
46
|
+
job_number = None
|
|
47
|
+
# 记录开始构建时间
|
|
48
|
+
start_time = time.time()
|
|
49
|
+
|
|
50
|
+
while True:
|
|
51
|
+
time.sleep(10)
|
|
52
|
+
if time.time() - start_time > wait_time:
|
|
53
|
+
print("构建超时,结束")
|
|
54
|
+
exit(1)
|
|
55
|
+
queue_info = server.get_queue_item(queue_num)
|
|
56
|
+
if queue_info['why'] is not None:
|
|
57
|
+
why = queue_info['why']
|
|
58
|
+
print(f'队列等待执行器...{why}')
|
|
59
|
+
elif 'executable' in queue_info:
|
|
60
|
+
job_number = queue_info['executable']['number']
|
|
61
|
+
print(f'开始执行任务...{job_number}')
|
|
62
|
+
break
|
|
63
|
+
else:
|
|
64
|
+
print('执行结束')
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
# 间隔x秒查询构建结果
|
|
68
|
+
while True:
|
|
69
|
+
time.sleep(10)
|
|
70
|
+
|
|
71
|
+
# 如果超过3分钟,直接结束
|
|
72
|
+
if time.time() - start_time > wait_time:
|
|
73
|
+
print("构建超时,结束")
|
|
74
|
+
exit(1)
|
|
75
|
+
headers = {
|
|
76
|
+
"n": str(job_number),
|
|
77
|
+
}
|
|
78
|
+
# 如果job_name包含 '/',则在把'/'替换为/job/
|
|
79
|
+
if '/' in job_name:
|
|
80
|
+
job_name_url = job_name.replace('/', '/job/')
|
|
81
|
+
url = f'{jenkins_server_url}/job/{job_name_url}/buildHistory/ajax'
|
|
82
|
+
else:
|
|
83
|
+
url = f'{jenkins_server_url}/job/{job_name}/buildHistory/ajax'
|
|
84
|
+
|
|
85
|
+
req = requests.Request(method='POST', url=url, headers=headers)
|
|
86
|
+
response = server.jenkins_open(req)
|
|
87
|
+
# 解析response包含的html
|
|
88
|
+
html_str = html.unescape(response)
|
|
89
|
+
if 'Expected build number' in html_str: # 正在等待中
|
|
90
|
+
print(f'\r等待中...', end='')
|
|
91
|
+
continue
|
|
92
|
+
elif ('预计剩余时间' in html_str) or ('Estimated remaining time' in html_str): # 是否包含 预计剩余时间
|
|
93
|
+
try:
|
|
94
|
+
# 取出 '<td style="width:' 和 '%;" class="progress-bar-done"></td>' 之间的字符串
|
|
95
|
+
progress = html_str.split('<td style="width:')[1].split('%;" class="progress-bar-done"></td>')[0]
|
|
96
|
+
# 打印进度更新最后一行
|
|
97
|
+
print(f'\r构建中...{progress}%', end='')
|
|
98
|
+
except IndexError:
|
|
99
|
+
print("无法从HTML字符串中提取进度信息")
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"发生错误: {e}")
|
|
102
|
+
continue
|
|
103
|
+
else:
|
|
104
|
+
print(f"\r构建结束,耗时 {(time.time() - start_time):.2f} 秒", end='\n')
|
|
105
|
+
# 查询构建结果
|
|
106
|
+
build_info = server.get_build_info(job_name, job_number)
|
|
107
|
+
if build_info['result'] == 'SUCCESS':
|
|
108
|
+
print(f"构建成功 {html.unescape(build_info['url'])}")
|
|
109
|
+
else:
|
|
110
|
+
print(f"构建失败({build_info['result']}) {html.unescape(build_info['url'])}")
|
|
111
|
+
print(f"原始 build_info:{build_info}")
|
|
112
|
+
print(f"原始 html:{html_str}")
|
|
113
|
+
return False
|
|
114
|
+
break
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# 获取 Portainer JWT Token
|
|
119
|
+
def get_jwt_token():
|
|
120
|
+
login_url = f"{portainer_api_url}/auth"
|
|
121
|
+
login_data = {
|
|
122
|
+
"Username": portainer_user_name,
|
|
123
|
+
"Password": portainer_user_pwd,
|
|
124
|
+
}
|
|
125
|
+
response = requests.post(login_url, data=json.dumps(login_data))
|
|
126
|
+
token = response.json()["jwt"]
|
|
127
|
+
print('获取token成功')
|
|
128
|
+
return token
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# 获取 Service ID
|
|
132
|
+
def get_server_id(token):
|
|
133
|
+
headers = {
|
|
134
|
+
"Authorization": f"Bearer {token}",
|
|
135
|
+
"Content-Type": "application/json"
|
|
136
|
+
}
|
|
137
|
+
# Get list of services
|
|
138
|
+
# services_response = requests.get(portainer_api_url + "/endpoints/1/docker/services", headers=headers) // dev改成了2
|
|
139
|
+
url = portainer_api_url + f'/endpoints/{portainer_endpoints}/docker/services'
|
|
140
|
+
services_response = requests.get(url, headers=headers)
|
|
141
|
+
services = services_response.json()
|
|
142
|
+
|
|
143
|
+
service_name = cd_env + portainer_server_suffix
|
|
144
|
+
# Find service with matching name
|
|
145
|
+
target_service_id = None
|
|
146
|
+
for service in services:
|
|
147
|
+
if service["Spec"]["Name"] == service_name:
|
|
148
|
+
target_service_id = service["ID"]
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
print(f'查询{service_name}的service_id:{target_service_id}')
|
|
152
|
+
return target_service_id
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# 获取 Service 的 WebHook
|
|
156
|
+
def get_service_web_hook():
|
|
157
|
+
pt_token = get_jwt_token()
|
|
158
|
+
service_id = get_server_id(pt_token)
|
|
159
|
+
|
|
160
|
+
headers = {
|
|
161
|
+
"Authorization": f"Bearer {pt_token}",
|
|
162
|
+
"Content-Type": "application/json"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# 查询service的webhook
|
|
166
|
+
# url = portainer_api_url + f'/webhooks?filters=%7B%22ResourceID%22:%22{service_id}%22,%22EndpointID%22:1%7D' // dev改成了2
|
|
167
|
+
url = portainer_api_url + f'/webhooks?filters=%7B%22ResourceID%22:%22{service_id}%22,%22EndpointID%22:{portainer_endpoints}%7D'
|
|
168
|
+
services_response = requests.get(url, headers=headers)
|
|
169
|
+
webhook_json = services_response.json()
|
|
170
|
+
target_webhook_token = None
|
|
171
|
+
# 是否存在webhook
|
|
172
|
+
if len(webhook_json) > 0:
|
|
173
|
+
target_webhook_token = webhook_json[0]['Token']
|
|
174
|
+
print('获取webhook_token:' + target_webhook_token)
|
|
175
|
+
|
|
176
|
+
# 尝试调用请求创建webhook
|
|
177
|
+
if target_webhook_token is None:
|
|
178
|
+
payload = {"ResourceID": service_id, "EndpointID": 2, "WebhookType": 1, "registryID": 0}
|
|
179
|
+
services_response = requests.post(portainer_api_url + "/webhooks", headers=headers, data=json.dumps(payload))
|
|
180
|
+
webhook_json = services_response.json()
|
|
181
|
+
if webhook_json is not None and 'Token' in webhook_json:
|
|
182
|
+
target_webhook_token = webhook_json['Token']
|
|
183
|
+
print('创建webhook_token结果:' + target_webhook_token)
|
|
184
|
+
|
|
185
|
+
return target_webhook_token
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# 启动webhook(dev1-15)
|
|
189
|
+
def trigger_webhook():
|
|
190
|
+
|
|
191
|
+
wh_token = get_service_web_hook()
|
|
192
|
+
|
|
193
|
+
if wh_token is None:
|
|
194
|
+
print(f"{cd_env}环境没有配置webhook,结束")
|
|
195
|
+
exit(0)
|
|
196
|
+
else:
|
|
197
|
+
print("开始触发webhook")
|
|
198
|
+
r = requests.post(portainer_api_url + '/webhooks/' + wh_token)
|
|
199
|
+
print("webhook触发结果:" + str(r.status_code))
|
|
200
|
+
|
|
201
|
+
# 通过service id 更新
|
|
202
|
+
def update_portainer():
|
|
203
|
+
|
|
204
|
+
pt_token = get_jwt_token()
|
|
205
|
+
service_id = get_server_id(pt_token)
|
|
206
|
+
|
|
207
|
+
headers = {
|
|
208
|
+
"Authorization": f"Bearer {pt_token}",
|
|
209
|
+
"Content-Type": "application/json"
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# 调用更新(PUT请求):
|
|
213
|
+
url = portainer_api_url + f'/endpoints/{portainer_endpoints}/forceupdateservice'
|
|
214
|
+
payload = {
|
|
215
|
+
"serviceID": service_id,
|
|
216
|
+
"pullImage": True,
|
|
217
|
+
}
|
|
218
|
+
services_response = requests.put(url, headers=headers, data=json.dumps(payload))
|
|
219
|
+
update_rsp_json = services_response.json()
|
|
220
|
+
print('更新结果:' + str(update_rsp_json))
|
|
221
|
+
|
|
222
|
+
# ~~~~~~~~~~~~~~~~~~ 开始输入 ~~~~~~~~~~~~~~~~~~~~~~~
|
|
223
|
+
|
|
224
|
+
# 打印欢迎信息
|
|
225
|
+
print('\n~~~~~~~~~~~~~~')
|
|
226
|
+
print('正在使用jenkins+portainer构建工具,目前支持dev和test环境的自动化构建+更新')
|
|
227
|
+
print('~~~~~~~~~~~~~~\n')
|
|
228
|
+
|
|
229
|
+
# 获取当前目录
|
|
230
|
+
current_dir = os.getcwd()
|
|
231
|
+
# 读取上一次构建的环境
|
|
232
|
+
with open(current_dir + '/last_cd_env.json', 'r', encoding='utf-8') as f:
|
|
233
|
+
cd_env_json = json.load(f)
|
|
234
|
+
|
|
235
|
+
# 从终端获取用户输入模式
|
|
236
|
+
build_mode = input(f"请输入模式 0-只构建 1-全构建+更新 2-构建api+更新 3-只更新(预设{cd_env_json['build_mode']}):", ) or cd_env_json['build_mode']
|
|
237
|
+
|
|
238
|
+
# 从终端获取用户输入cd_env
|
|
239
|
+
cd_env = input(f"请输入环境(预设:{cd_env_json['cd_env']}):", ) or cd_env_json['cd_env']
|
|
240
|
+
|
|
241
|
+
# 从终端获取用户输入的分支
|
|
242
|
+
core_param_branch = input(f"请输入core分支(预设:{cd_env_json['core_param_branch']}):") or cd_env_json['core_param_branch']
|
|
243
|
+
|
|
244
|
+
# 从终端输入api分支
|
|
245
|
+
api_param_branch = input(f"请输入api分支(预设:{cd_env_json['api_param_branch']}):") or cd_env_json['api_param_branch']
|
|
246
|
+
|
|
247
|
+
# 从终端输入api文件夹名
|
|
248
|
+
# 读取上一次构建的分支
|
|
249
|
+
api_param_folder = input(f"请输入定制工程文件夹名(空格或None表示不需要,预设:{cd_env_json['api_param_folder']}):") or cd_env_json['api_param_folder']
|
|
250
|
+
|
|
251
|
+
# 校验模式:
|
|
252
|
+
if build_mode is None or build_mode not in ('0', '1', '2', '3'):
|
|
253
|
+
print("模式错误,结束")
|
|
254
|
+
exit(1)
|
|
255
|
+
# 校验环境,如果不是dev或test开头的,直接退出
|
|
256
|
+
if cd_env is None or not cd_env.startswith('dev') and not cd_env.startswith('test'):
|
|
257
|
+
print("环境错误,结束")
|
|
258
|
+
exit(1)
|
|
259
|
+
# 校验分支
|
|
260
|
+
if core_param_branch is None:
|
|
261
|
+
print("分支错误,结束")
|
|
262
|
+
exit(1)
|
|
263
|
+
if api_param_branch is None:
|
|
264
|
+
print("分支错误,结束")
|
|
265
|
+
exit(1)
|
|
266
|
+
if api_param_folder is None or api_param_folder == '' or api_param_folder == ' ' or api_param_folder == 'none' or api_param_folder == 'None':
|
|
267
|
+
api_param_folder = None
|
|
268
|
+
# 如果dev44及以上环境,只能使用模式0:
|
|
269
|
+
if cd_env.startswith('dev') and int(cd_env[3:]) >= 44 and build_mode != '0':
|
|
270
|
+
print("dev43及以上环境只能使用模式0,结束")
|
|
271
|
+
exit(1)
|
|
272
|
+
|
|
273
|
+
# 保存到本地环境
|
|
274
|
+
cd_env_json['cd_env'] = cd_env
|
|
275
|
+
cd_env_json['core_param_branch'] = core_param_branch
|
|
276
|
+
cd_env_json['api_param_branch'] = api_param_branch
|
|
277
|
+
cd_env_json['api_param_folder'] = api_param_folder
|
|
278
|
+
cd_env_json['build_mode'] = build_mode
|
|
279
|
+
json.dump(cd_env_json, open(current_dir + '/last_cd_env.json', 'w', encoding='utf-8'), ensure_ascii=False)
|
|
280
|
+
|
|
281
|
+
# exit(0)
|
|
282
|
+
|
|
283
|
+
# 根据环境输入,设置前缀
|
|
284
|
+
env_prefix = 'dev' if cd_env.startswith('dev') else 'test'
|
|
285
|
+
|
|
286
|
+
# 读取上一次构建的环境
|
|
287
|
+
with open(current_dir + '/env_param.json', 'r', encoding='utf-8') as f:
|
|
288
|
+
env_param = json.load(f)
|
|
289
|
+
|
|
290
|
+
jenkins_user_id = env_param['jenkins_user_id'][env_prefix]
|
|
291
|
+
jenkins_api_token = env_param['jenkins_api_token'][env_prefix]
|
|
292
|
+
core_job = env_param['core_job'][env_prefix]
|
|
293
|
+
api_job = env_param['api_job'][env_prefix]
|
|
294
|
+
if api_param_folder is not None:
|
|
295
|
+
core_job = api_param_folder + '/' + env_param['core_job_custom'][env_prefix]
|
|
296
|
+
api_job = api_param_folder + '/' + env_param['api_job_custom'][env_prefix]
|
|
297
|
+
|
|
298
|
+
# portainer配置(如果cd_env > dev15,使用另一套配置)
|
|
299
|
+
if cd_env.startswith('dev') and int(cd_env[3:]) > 15:
|
|
300
|
+
portainer_user_name = env_param['portainer_user_name']['dev16']
|
|
301
|
+
portainer_user_pwd = env_param['portainer_user_pwd']['dev16']
|
|
302
|
+
portainer_api_url = env_param['portainer_api_url']['dev16']
|
|
303
|
+
portainer_endpoints = env_param['portainer_endpoints']['dev16']
|
|
304
|
+
portainer_server_suffix = env_param['portainer_server_suffix']['dev16']
|
|
305
|
+
else:
|
|
306
|
+
portainer_api_url = env_param['portainer_api_url'][env_prefix]
|
|
307
|
+
portainer_user_name = env_param['portainer_user_name'][env_prefix]
|
|
308
|
+
portainer_user_pwd = env_param['portainer_user_pwd'][env_prefix]
|
|
309
|
+
portainer_endpoints = env_param['portainer_endpoints'][env_prefix]
|
|
310
|
+
portainer_server_suffix = env_param['portainer_server_suffix'][env_prefix]
|
|
311
|
+
|
|
312
|
+
# ~~~~~~~~~~~~~~~~~ 开始打包 ~~~~~~~~~~~~~~~~~~~~~~~~
|
|
313
|
+
|
|
314
|
+
print('\n~~~~~~~~~~~~~~\n')
|
|
315
|
+
print('开始打包')
|
|
316
|
+
print("链接jenkins:" + jenkins_server_url)
|
|
317
|
+
|
|
318
|
+
# 实例化jenkins对象,连接远程的jenkins master server
|
|
319
|
+
j_server = jenkins.Jenkins(url=jenkins_server_url, username=jenkins_user_id, password=jenkins_api_token)
|
|
320
|
+
print("jenkins信息:" + j_server.get_whoami()['fullName'])
|
|
321
|
+
|
|
322
|
+
# 通过参数构建core
|
|
323
|
+
if int(build_mode) < 2:
|
|
324
|
+
print('开始构建core:' + core_job + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
|
|
325
|
+
rs = do_job(j_server, core_job, core_param_branch, cd_env, core_timeout)
|
|
326
|
+
if rs is False:
|
|
327
|
+
print('构建core失败,结束')
|
|
328
|
+
exit(1)
|
|
329
|
+
time.sleep(10)
|
|
330
|
+
|
|
331
|
+
if int(build_mode) < 3:
|
|
332
|
+
print('开始构建api:' + api_job + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
|
|
333
|
+
rs = do_job(j_server, api_job, api_param_branch, cd_env, api_timeout)
|
|
334
|
+
# 如果构建api失败,再次构建
|
|
335
|
+
# if rs is False:
|
|
336
|
+
# print('第一次构建api失败,再次构建')
|
|
337
|
+
# rs = do_job_1(j_server, api_job, api_param_branch, cd_env, api_timeout)
|
|
338
|
+
if rs is False:
|
|
339
|
+
print('构建api失败,结束')
|
|
340
|
+
exit(1)
|
|
341
|
+
time.sleep(2)
|
|
342
|
+
|
|
343
|
+
print('结束打包' + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
|
|
344
|
+
|
|
345
|
+
# ~~~~~~~~~~~~~~~~~ 开始更新 ~~~~~~~~~~~~~~~~~~~~~~~~
|
|
346
|
+
|
|
347
|
+
# 开始更新portainer
|
|
348
|
+
if int(build_mode) == 0:
|
|
349
|
+
print('\n~~~~~~~ 请自行更新portainer ~~~~~~~\n')
|
|
350
|
+
else:
|
|
351
|
+
print('\n~~~~~~~~~~~~~~\n')
|
|
352
|
+
print('开始更新portainer:' + portainer_api_url + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
|
|
353
|
+
# portainer配置(如果cd_env > dev15,使用update)
|
|
354
|
+
if cd_env.startswith('dev') and int(cd_env[3:]) > 15:
|
|
355
|
+
# 通过update更新
|
|
356
|
+
update_portainer()
|
|
357
|
+
else:
|
|
358
|
+
# 触发webhook
|
|
359
|
+
trigger_webhook()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
exit(0)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jenkins-deploy
|
|
3
|
+
description: |
|
|
4
|
+
Jenkins + Portainer 自动打包部署技能。通过 Python 脚本调用 Jenkins API 构建项目,并触发 Portainer Webhook/Update 完成容器更新。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- 需要将代码打包部署到 dev/test 环境
|
|
8
|
+
- 需要触发 Jenkins 构建 core 或 api 项目
|
|
9
|
+
- 需要更新 Portainer 容器服务
|
|
10
|
+
- 需要查看或修改构建配置(分支、环境、模式)
|
|
11
|
+
- 定制项目的打包部署
|
|
12
|
+
|
|
13
|
+
触发词:打包、部署、Jenkins、构建、Portainer、发布到dev、发布到test、更新环境、自动部署
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Jenkins + Portainer 自动打包部署
|
|
17
|
+
|
|
18
|
+
## 概述
|
|
19
|
+
|
|
20
|
+
项目使用 `jenkins/` 目录下的 Python 脚本实现自动化构建部署,流程:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Jenkins 构建 core → Jenkins 构建 api → Portainer 更新容器
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 文件位置
|
|
27
|
+
|
|
28
|
+
| 文件 | 作用 |
|
|
29
|
+
|------|------|
|
|
30
|
+
| `jenkins/jk_build.py` | 主构建脚本 |
|
|
31
|
+
| `jenkins/env_param.json` | 环境配置(Jenkins/Portainer 连接参数) |
|
|
32
|
+
| `jenkins/last_cd_env.json` | 上次构建参数(自动保存) |
|
|
33
|
+
|
|
34
|
+
## 构建模式
|
|
35
|
+
|
|
36
|
+
| 模式 | 说明 | 执行步骤 |
|
|
37
|
+
|------|------|---------|
|
|
38
|
+
| `0` | 只构建 | 构建 core + api,不更新 Portainer |
|
|
39
|
+
| `1` | 全构建+更新 | 构建 core + api → 触发 Portainer 更新 |
|
|
40
|
+
| `2` | 构建 api+更新 | 跳过 core,构建 api → 触发 Portainer 更新 |
|
|
41
|
+
| `3` | 只更新 | 不构建,直接触发 Portainer 更新 |
|
|
42
|
+
|
|
43
|
+
## 环境支持
|
|
44
|
+
|
|
45
|
+
| 环境 | 前缀 | Jenkins Job | Portainer |
|
|
46
|
+
|------|------|-------------|-----------|
|
|
47
|
+
| 开发环境 | `dev` + 编号 | `dev-tengyun-core` / `dev-tengyun-yunshitang-api` | devops-dev.xnzn.net |
|
|
48
|
+
| 测试环境 | `test` + 编号 | `test-tengyun-core` / `test-tengyun-yunshitang-api` | devops-test.xnzn.net |
|
|
49
|
+
| dev16+ | `dev16` ~ `dev43` | 同 dev | xnzn-dev.xnzn.net(使用 forceupdateservice) |
|
|
50
|
+
| dev44+ | `dev44` 及以上 | 只支持模式 0 | 需手动更新 |
|
|
51
|
+
|
|
52
|
+
## 定制项目支持
|
|
53
|
+
|
|
54
|
+
当指定 `api_param_folder`(定制工程文件夹名)时,Jenkins Job 路径变为:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
{folder_name}/dev-后端-core # 替代 dev-tengyun-core
|
|
58
|
+
{folder_name}/dev-后端-api # 替代 dev-tengyun-yunshitang-api
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 使用方式
|
|
62
|
+
|
|
63
|
+
### 直接运行脚本
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd jenkins && python jk_build.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
脚本会交互式询问:
|
|
70
|
+
1. **模式**(0/1/2/3)
|
|
71
|
+
2. **环境**(dev1, dev2, test1 等)
|
|
72
|
+
3. **core 分支**(如 release_5.56.0)
|
|
73
|
+
4. **api 分支**(如 master)
|
|
74
|
+
5. **定制工程文件夹**(可选,空格或 None 跳过)
|
|
75
|
+
|
|
76
|
+
### 通过 Claude 辅助部署
|
|
77
|
+
|
|
78
|
+
当用户说"打包到 devX"时:
|
|
79
|
+
|
|
80
|
+
1. **读取** `jenkins/last_cd_env.json` 获取上次构建参数
|
|
81
|
+
2. **确认参数**:环境、分支、模式、是否定制项目
|
|
82
|
+
3. **修改** `last_cd_env.json` 写入新参数
|
|
83
|
+
4. **执行** `cd jenkins && python jk_build.py`(脚本读取预设值,用户直接回车即可)
|
|
84
|
+
|
|
85
|
+
## 配置文件说明
|
|
86
|
+
|
|
87
|
+
### last_cd_env.json(构建参数)
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"build_mode": "0",
|
|
92
|
+
"cd_env": "dev63",
|
|
93
|
+
"core_param_branch": "release_5.56.0",
|
|
94
|
+
"api_param_branch": "master",
|
|
95
|
+
"api_param_folder": null
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| 字段 | 说明 |
|
|
100
|
+
|------|------|
|
|
101
|
+
| `build_mode` | 构建模式(0/1/2/3) |
|
|
102
|
+
| `cd_env` | 目标环境(如 dev1, test2) |
|
|
103
|
+
| `core_param_branch` | core 仓库分支 |
|
|
104
|
+
| `api_param_branch` | api 仓库分支 |
|
|
105
|
+
| `api_param_folder` | 定制项目文件夹(null 表示标准项目) |
|
|
106
|
+
|
|
107
|
+
### env_param.json(环境连接配置)
|
|
108
|
+
|
|
109
|
+
包含 Jenkins 和 Portainer 的连接参数(用户名、API Token、服务地址等),按 dev/test 环境分组。
|
|
110
|
+
|
|
111
|
+
> **注意**:此文件包含敏感凭证,不要将内容输出到对话中。
|
|
112
|
+
|
|
113
|
+
## 构建流程详解
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
1. 连接 Jenkins(ci.xnzn.net)
|
|
117
|
+
2. 构建 core(参数:BRANCH + VERSION=环境名)
|
|
118
|
+
└─ 轮询构建进度,超时 10 分钟
|
|
119
|
+
3. 构建 api(参数同上)
|
|
120
|
+
└─ 轮询构建进度,超时 5 分钟
|
|
121
|
+
4. 更新 Portainer:
|
|
122
|
+
├─ dev1~15:Webhook 触发
|
|
123
|
+
│ └─ 获取 JWT → 查 Service ID → 获取/创建 Webhook → POST 触发
|
|
124
|
+
└─ dev16+:Force Update
|
|
125
|
+
└─ 获取 JWT → 查 Service ID → PUT forceupdateservice
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## 常见操作
|
|
129
|
+
|
|
130
|
+
### 只改了 api 代码,快速部署
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# 修改 last_cd_env.json 的 build_mode 为 "2",然后运行
|
|
134
|
+
cd jenkins && python jk_build.py
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 只需要更新容器(已在 Jenkins 手动构建完)
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# build_mode 设为 "3"
|
|
141
|
+
cd jenkins && python jk_build.py
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 部署定制项目
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
// last_cd_env.json
|
|
148
|
+
{
|
|
149
|
+
"api_param_folder": "leniu-tengyun-wuhanxieheyiyuan"
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 首次初始化(团队成员)
|
|
154
|
+
|
|
155
|
+
当用户说"初始化部署环境"或检测到 `jenkins/` 目录不存在时,执行以下步骤:
|
|
156
|
+
|
|
157
|
+
### 自动初始化流程
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# 1. 创建 jenkins 目录
|
|
161
|
+
mkdir -p jenkins
|
|
162
|
+
|
|
163
|
+
# 2. 从技能模板复制文件(SKILL_DIR 为技能目录路径)
|
|
164
|
+
cp .claude/skills/jenkins-deploy/assets/jk_build.py jenkins/jk_build.py
|
|
165
|
+
cp .claude/skills/jenkins-deploy/assets/last_cd_env.template.json jenkins/last_cd_env.json
|
|
166
|
+
|
|
167
|
+
# 3. 环境配置需要用户提供(含敏感凭证)
|
|
168
|
+
cp .claude/skills/jenkins-deploy/assets/env_param.template.json jenkins/env_param.json
|
|
169
|
+
|
|
170
|
+
# 4. 安装依赖
|
|
171
|
+
pip install python-jenkins requests
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 配置凭证
|
|
175
|
+
|
|
176
|
+
初始化后,`jenkins/env_param.json` 中的占位符需要替换为真实值:
|
|
177
|
+
|
|
178
|
+
| 占位符 | 说明 | 获取方式 |
|
|
179
|
+
|--------|------|---------|
|
|
180
|
+
| `__JENKINS_DEV_USER__` | Jenkins dev 账号 | 向团队负责人获取 |
|
|
181
|
+
| `__JENKINS_DEV_TOKEN__` | Jenkins dev API Token | Jenkins → 用户 → 设置 → API Token |
|
|
182
|
+
| `__JENKINS_TEST_USER__` | Jenkins test 账号 | 同上 |
|
|
183
|
+
| `__JENKINS_TEST_TOKEN__` | Jenkins test API Token | 同上 |
|
|
184
|
+
| `__PORTAINER_*_USER__` | Portainer 用户名 | 向团队负责人获取 |
|
|
185
|
+
| `__PORTAINER_*_PWD__` | Portainer 密码 | 向团队负责人获取 |
|
|
186
|
+
|
|
187
|
+
**快捷方式**:如果团队已有成员配置好,可直接拷贝对方的 `env_param.json` 文件。
|
|
188
|
+
|
|
189
|
+
### AI 初始化行为
|
|
190
|
+
|
|
191
|
+
当技能被触发但 `jenkins/` 不存在时:
|
|
192
|
+
1. 提示用户"检测到尚未初始化部署环境"
|
|
193
|
+
2. 询问是否执行初始化
|
|
194
|
+
3. 执行上述复制步骤
|
|
195
|
+
4. 提示用户填写凭证(或拷贝已有配置)
|
|
196
|
+
|
|
197
|
+
## 依赖
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pip install python-jenkins requests
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## 注意
|
|
204
|
+
|
|
205
|
+
- 本技能用于 dev/test 环境部署,**不涉及生产环境**
|
|
206
|
+
- 如果是 Git 提交/分支管理,请使用 `git-workflow` 技能
|
|
207
|
+
- 如果是代码构建错误排查,请使用 `bug-detective` 技能
|
|
@@ -417,6 +417,10 @@ const skillMap = [
|
|
|
417
417
|
{
|
|
418
418
|
name: 'skill-creator',
|
|
419
419
|
keywords: ['创建技能', '技能模板', '技能脚手架', 'skill scaffold', '新建skill']
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'jenkins-deploy',
|
|
423
|
+
keywords: ['打包', '部署', 'Jenkins', '构建', 'Portainer', '发布到dev', '发布到test', '更新环境', '自动部署']
|
|
420
424
|
}
|
|
421
425
|
];
|
|
422
426
|
|
|
@@ -87,6 +87,7 @@ alwaysApply: true
|
|
|
87
87
|
| 多租户/租户隔离/TenantEntity/tenantId/SaaS | `.cursor/skills/tenant-management/SKILL.md` |
|
|
88
88
|
| Element UI/前端组件/el-table/el-form/管理页面 | `.cursor/skills/ui-pc/SKILL.md` |
|
|
89
89
|
| Vuex/store/mapState/mapActions/状态管理 | `.cursor/skills/store-pc/SKILL.md` |
|
|
90
|
+
| 打包/部署/Jenkins/构建/Portainer/发布到dev/发布到test/自动部署 | `.cursor/skills/jenkins-deploy/SKILL.md` |
|
|
90
91
|
|
|
91
92
|
### 执行规则
|
|
92
93
|
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jenkins-deploy
|
|
3
|
+
description: |
|
|
4
|
+
Jenkins + Portainer 自动打包部署技能。通过 Python 脚本调用 Jenkins API 构建项目,并触发 Portainer Webhook/Update 完成容器更新。
|
|
5
|
+
|
|
6
|
+
触发场景:
|
|
7
|
+
- 需要将代码打包部署到 dev/test 环境
|
|
8
|
+
- 需要触发 Jenkins 构建 core 或 api 项目
|
|
9
|
+
- 需要更新 Portainer 容器服务
|
|
10
|
+
- 需要查看或修改构建配置(分支、环境、模式)
|
|
11
|
+
- 定制项目的打包部署
|
|
12
|
+
|
|
13
|
+
触发词:打包、部署、Jenkins、构建、Portainer、发布到dev、发布到test、更新环境、自动部署
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Jenkins + Portainer 自动打包部署
|
|
17
|
+
|
|
18
|
+
## 概述
|
|
19
|
+
|
|
20
|
+
项目使用 `jenkins/` 目录下的 Python 脚本实现自动化构建部署,流程:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Jenkins 构建 core → Jenkins 构建 api → Portainer 更新容器
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 文件位置
|
|
27
|
+
|
|
28
|
+
| 文件 | 作用 |
|
|
29
|
+
|------|------|
|
|
30
|
+
| `jenkins/jk_build.py` | 主构建脚本 |
|
|
31
|
+
| `jenkins/env_param.json` | 环境配置(Jenkins/Portainer 连接参数) |
|
|
32
|
+
| `jenkins/last_cd_env.json` | 上次构建参数(自动保存) |
|
|
33
|
+
|
|
34
|
+
## 构建模式
|
|
35
|
+
|
|
36
|
+
| 模式 | 说明 | 执行步骤 |
|
|
37
|
+
|------|------|---------|
|
|
38
|
+
| `0` | 只构建 | 构建 core + api,不更新 Portainer |
|
|
39
|
+
| `1` | 全构建+更新 | 构建 core + api → 触发 Portainer 更新 |
|
|
40
|
+
| `2` | 构建 api+更新 | 跳过 core,构建 api → 触发 Portainer 更新 |
|
|
41
|
+
| `3` | 只更新 | 不构建,直接触发 Portainer 更新 |
|
|
42
|
+
|
|
43
|
+
## 环境支持
|
|
44
|
+
|
|
45
|
+
| 环境 | 前缀 | Jenkins Job | Portainer |
|
|
46
|
+
|------|------|-------------|-----------|
|
|
47
|
+
| 开发环境 | `dev` + 编号 | `dev-tengyun-core` / `dev-tengyun-yunshitang-api` | devops-dev.xnzn.net |
|
|
48
|
+
| 测试环境 | `test` + 编号 | `test-tengyun-core` / `test-tengyun-yunshitang-api` | devops-test.xnzn.net |
|
|
49
|
+
| dev16+ | `dev16` ~ `dev43` | 同 dev | xnzn-dev.xnzn.net(使用 forceupdateservice) |
|
|
50
|
+
| dev44+ | `dev44` 及以上 | 只支持模式 0 | 需手动更新 |
|
|
51
|
+
|
|
52
|
+
## 定制项目支持
|
|
53
|
+
|
|
54
|
+
当指定 `api_param_folder`(定制工程文件夹名)时,Jenkins Job 路径变为:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
{folder_name}/dev-后端-core # 替代 dev-tengyun-core
|
|
58
|
+
{folder_name}/dev-后端-api # 替代 dev-tengyun-yunshitang-api
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 使用方式
|
|
62
|
+
|
|
63
|
+
### 直接运行脚本
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd jenkins && python jk_build.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
脚本会交互式询问:
|
|
70
|
+
1. **模式**(0/1/2/3)
|
|
71
|
+
2. **环境**(dev1, dev2, test1 等)
|
|
72
|
+
3. **core 分支**(如 release_5.56.0)
|
|
73
|
+
4. **api 分支**(如 master)
|
|
74
|
+
5. **定制工程文件夹**(可选,空格或 None 跳过)
|
|
75
|
+
|
|
76
|
+
### 通过 Claude 辅助部署
|
|
77
|
+
|
|
78
|
+
当用户说"打包到 devX"时:
|
|
79
|
+
|
|
80
|
+
1. **读取** `jenkins/last_cd_env.json` 获取上次构建参数
|
|
81
|
+
2. **确认参数**:环境、分支、模式、是否定制项目
|
|
82
|
+
3. **修改** `last_cd_env.json` 写入新参数
|
|
83
|
+
4. **执行** `cd jenkins && python jk_build.py`(脚本读取预设值,用户直接回车即可)
|
|
84
|
+
|
|
85
|
+
## 配置文件说明
|
|
86
|
+
|
|
87
|
+
### last_cd_env.json(构建参数)
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"build_mode": "0",
|
|
92
|
+
"cd_env": "dev63",
|
|
93
|
+
"core_param_branch": "release_5.56.0",
|
|
94
|
+
"api_param_branch": "master",
|
|
95
|
+
"api_param_folder": null
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| 字段 | 说明 |
|
|
100
|
+
|------|------|
|
|
101
|
+
| `build_mode` | 构建模式(0/1/2/3) |
|
|
102
|
+
| `cd_env` | 目标环境(如 dev1, test2) |
|
|
103
|
+
| `core_param_branch` | core 仓库分支 |
|
|
104
|
+
| `api_param_branch` | api 仓库分支 |
|
|
105
|
+
| `api_param_folder` | 定制项目文件夹(null 表示标准项目) |
|
|
106
|
+
|
|
107
|
+
### env_param.json(环境连接配置)
|
|
108
|
+
|
|
109
|
+
包含 Jenkins 和 Portainer 的连接参数(用户名、API Token、服务地址等),按 dev/test 环境分组。
|
|
110
|
+
|
|
111
|
+
> **注意**:此文件包含敏感凭证,不要将内容输出到对话中。
|
|
112
|
+
|
|
113
|
+
## 构建流程详解
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
1. 连接 Jenkins(ci.xnzn.net)
|
|
117
|
+
2. 构建 core(参数:BRANCH + VERSION=环境名)
|
|
118
|
+
└─ 轮询构建进度,超时 10 分钟
|
|
119
|
+
3. 构建 api(参数同上)
|
|
120
|
+
└─ 轮询构建进度,超时 5 分钟
|
|
121
|
+
4. 更新 Portainer:
|
|
122
|
+
├─ dev1~15:Webhook 触发
|
|
123
|
+
│ └─ 获取 JWT → 查 Service ID → 获取/创建 Webhook → POST 触发
|
|
124
|
+
└─ dev16+:Force Update
|
|
125
|
+
└─ 获取 JWT → 查 Service ID → PUT forceupdateservice
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## 常见操作
|
|
129
|
+
|
|
130
|
+
### 只改了 api 代码,快速部署
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# 修改 last_cd_env.json 的 build_mode 为 "2",然后运行
|
|
134
|
+
cd jenkins && python jk_build.py
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 只需要更新容器(已在 Jenkins 手动构建完)
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# build_mode 设为 "3"
|
|
141
|
+
cd jenkins && python jk_build.py
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 部署定制项目
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
// last_cd_env.json
|
|
148
|
+
{
|
|
149
|
+
"api_param_folder": "leniu-tengyun-wuhanxieheyiyuan"
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 首次初始化(团队成员)
|
|
154
|
+
|
|
155
|
+
当用户说"初始化部署环境"或检测到 `jenkins/` 目录不存在时,执行以下步骤:
|
|
156
|
+
|
|
157
|
+
### 自动初始化流程
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# 1. 创建 jenkins 目录
|
|
161
|
+
mkdir -p jenkins
|
|
162
|
+
|
|
163
|
+
# 2. 从技能模板复制文件(SKILL_DIR 为技能目录路径)
|
|
164
|
+
cp .claude/skills/jenkins-deploy/assets/jk_build.py jenkins/jk_build.py
|
|
165
|
+
cp .claude/skills/jenkins-deploy/assets/last_cd_env.template.json jenkins/last_cd_env.json
|
|
166
|
+
|
|
167
|
+
# 3. 环境配置需要用户提供(含敏感凭证)
|
|
168
|
+
cp .claude/skills/jenkins-deploy/assets/env_param.template.json jenkins/env_param.json
|
|
169
|
+
|
|
170
|
+
# 4. 安装依赖
|
|
171
|
+
pip install python-jenkins requests
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 配置凭证
|
|
175
|
+
|
|
176
|
+
初始化后,`jenkins/env_param.json` 中的占位符需要替换为真实值:
|
|
177
|
+
|
|
178
|
+
| 占位符 | 说明 | 获取方式 |
|
|
179
|
+
|--------|------|---------|
|
|
180
|
+
| `__JENKINS_DEV_USER__` | Jenkins dev 账号 | 向团队负责人获取 |
|
|
181
|
+
| `__JENKINS_DEV_TOKEN__` | Jenkins dev API Token | Jenkins → 用户 → 设置 → API Token |
|
|
182
|
+
| `__JENKINS_TEST_USER__` | Jenkins test 账号 | 同上 |
|
|
183
|
+
| `__JENKINS_TEST_TOKEN__` | Jenkins test API Token | 同上 |
|
|
184
|
+
| `__PORTAINER_*_USER__` | Portainer 用户名 | 向团队负责人获取 |
|
|
185
|
+
| `__PORTAINER_*_PWD__` | Portainer 密码 | 向团队负责人获取 |
|
|
186
|
+
|
|
187
|
+
**快捷方式**:如果团队已有成员配置好,可直接拷贝对方的 `env_param.json` 文件。
|
|
188
|
+
|
|
189
|
+
### AI 初始化行为
|
|
190
|
+
|
|
191
|
+
当技能被触发但 `jenkins/` 不存在时:
|
|
192
|
+
1. 提示用户"检测到尚未初始化部署环境"
|
|
193
|
+
2. 询问是否执行初始化
|
|
194
|
+
3. 执行上述复制步骤
|
|
195
|
+
4. 提示用户填写凭证(或拷贝已有配置)
|
|
196
|
+
|
|
197
|
+
## 依赖
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pip install python-jenkins requests
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## 注意
|
|
204
|
+
|
|
205
|
+
- 本技能用于 dev/test 环境部署,**不涉及生产环境**
|
|
206
|
+
- 如果是 Git 提交/分支管理,请使用 `git-workflow` 技能
|
|
207
|
+
- 如果是代码构建错误排查,请使用 `bug-detective` 技能
|
package/AGENTS.md
CHANGED
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
| `collaborating-with-gemini` | 与 Gemini 协同设计 |
|
|
95
95
|
| `codex-code-review` | 代码审查 |
|
|
96
96
|
| `leniu-marketing-scenario` | 营销规则开发(折扣/满减/限额/限次/补贴/充值赠送/扣款/就餐规则) |
|
|
97
|
+
| `jenkins-deploy` | 打包部署(Jenkins 构建 + Portainer 更新,dev/test 环境) |
|
|
97
98
|
|
|
98
99
|
### OpenSpec 规格驱动开发技能(SDD)
|
|
99
100
|
|
package/bin/index.js
CHANGED
|
@@ -586,6 +586,24 @@ function showDoneHint(toolKey) {
|
|
|
586
586
|
console.log(` 2. 在 Codex 中使用 .codex/skills/ 下的技能`);
|
|
587
587
|
console.log('');
|
|
588
588
|
}
|
|
589
|
+
showJenkinsHint();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** 检测 jenkins/ 是否已初始化,提示用户配置部署环境 */
|
|
593
|
+
function showJenkinsHint() {
|
|
594
|
+
const jenkinsDir = path.join(targetDir, 'jenkins');
|
|
595
|
+
const envParamFile = path.join(jenkinsDir, 'env_param.json');
|
|
596
|
+
const skillAssetsDir = path.join(targetDir, '.claude', 'skills', 'jenkins-deploy', 'assets');
|
|
597
|
+
|
|
598
|
+
// 技能 assets 存在但 jenkins/ 未初始化
|
|
599
|
+
if (fs.existsSync(skillAssetsDir) && !fs.existsSync(envParamFile)) {
|
|
600
|
+
console.log(fmt('yellow', fmt('bold', '📦 Jenkins 部署环境未初始化')));
|
|
601
|
+
console.log(` 项目包含 ${fmt('bold', 'jenkins-deploy')} 技能,但 jenkins/ 目录尚未配置。`);
|
|
602
|
+
console.log(` 初始化方式:`);
|
|
603
|
+
console.log(` 方式 1:在 AI 对话中说 ${fmt('bold', '"初始化部署环境"')}`);
|
|
604
|
+
console.log(` 方式 2:从团队成员处拷贝 ${fmt('bold', 'jenkins/')} 目录`);
|
|
605
|
+
console.log('');
|
|
606
|
+
}
|
|
589
607
|
}
|
|
590
608
|
|
|
591
609
|
function run(selectedTool) {
|
|
@@ -748,6 +766,7 @@ function runUpdate(selectedTool) {
|
|
|
748
766
|
console.log(` 强制更新保留文件: ${fmt('bold', hintCmd('update --force'))}`);
|
|
749
767
|
}
|
|
750
768
|
console.log('');
|
|
769
|
+
showJenkinsHint();
|
|
751
770
|
|
|
752
771
|
if (totalFailed > 0) process.exitCode = 1;
|
|
753
772
|
}
|