ai-engineering-init 1.17.0 → 1.17.2
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 +134 -0
- package/.claude/skills/jenkins-deploy/assets/env_param.template.json +51 -0
- package/.claude/skills/jenkins-deploy/assets/jk_build.py +400 -0
- package/.codex/skills/jenkins-deploy/SKILL.md +134 -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 +134 -0
- package/AGENTS.md +1 -0
- package/bin/index.js +25 -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,134 @@
|
|
|
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
|
+
```
|
|
21
|
+
技能目录(随框架更新,不复制到项目):
|
|
22
|
+
~/.claude/skills/jenkins-deploy/assets/jk_build.py ← 构建脚本
|
|
23
|
+
|
|
24
|
+
全局配置(一次性配置,所有项目共享):
|
|
25
|
+
~/.claude/jenkins-config.json ← 凭证(Jenkins/Portainer)
|
|
26
|
+
|
|
27
|
+
项目本地(自动生成):
|
|
28
|
+
jenkins/last_cd_env.json ← 构建状态(环境、分支、模式)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
脚本按 **本地 `.claude/` > 全局 `~/.claude/`** 优先级查找 `jenkins-config.json`。
|
|
32
|
+
|
|
33
|
+
## 构建模式
|
|
34
|
+
|
|
35
|
+
| 模式 | 说明 | 执行步骤 |
|
|
36
|
+
|------|------|---------|
|
|
37
|
+
| `0` | 只构建 | 构建 core + api,不更新 Portainer |
|
|
38
|
+
| `1` | 全构建+更新 | 构建 core + api → 触发 Portainer 更新 |
|
|
39
|
+
| `2` | 构建 api+更新 | 跳过 core,构建 api → 触发 Portainer 更新 |
|
|
40
|
+
| `3` | 只更新 | 不构建,直接触发 Portainer 更新 |
|
|
41
|
+
|
|
42
|
+
## 环境支持
|
|
43
|
+
|
|
44
|
+
| 环境 | 前缀 | Portainer 更新方式 |
|
|
45
|
+
|------|------|-------------------|
|
|
46
|
+
| dev1~15 | `dev` | Webhook 触发 |
|
|
47
|
+
| dev16~43 | `dev` | Force Update(xnzn-dev.xnzn.net) |
|
|
48
|
+
| dev44+ | `dev` | 只支持模式 0(手动更新) |
|
|
49
|
+
| test | `test` | Webhook 触发 |
|
|
50
|
+
|
|
51
|
+
## 使用方式
|
|
52
|
+
|
|
53
|
+
### 运行构建脚本
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python ~/.claude/skills/jenkins-deploy/assets/jk_build.py
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
脚本交互式询问:模式 → 环境 → core 分支 → api 分支 → 定制工程文件夹
|
|
60
|
+
|
|
61
|
+
### 通过 AI 辅助部署
|
|
62
|
+
|
|
63
|
+
当用户说"打包到 devX"时:
|
|
64
|
+
|
|
65
|
+
1. 读取 `jenkins/last_cd_env.json` 获取上次参数
|
|
66
|
+
2. 确认参数:环境、分支、模式
|
|
67
|
+
3. 修改 `jenkins/last_cd_env.json` 写入新参数
|
|
68
|
+
4. 执行 `python ~/.claude/skills/jenkins-deploy/assets/jk_build.py`
|
|
69
|
+
|
|
70
|
+
## 配置文件
|
|
71
|
+
|
|
72
|
+
### jenkins-config.json(凭证,全局)
|
|
73
|
+
|
|
74
|
+
位置:`~/.claude/jenkins-config.json`(或本地 `.claude/jenkins-config.json`)
|
|
75
|
+
|
|
76
|
+
模板:`.claude/skills/jenkins-deploy/assets/env_param.template.json`
|
|
77
|
+
|
|
78
|
+
> **注意**:此文件包含敏感凭证,不要将内容输出到对话中。
|
|
79
|
+
|
|
80
|
+
### last_cd_env.json(构建状态,项目本地)
|
|
81
|
+
|
|
82
|
+
位置:`jenkins/last_cd_env.json`(脚本自动创建,无需手动初始化)
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"build_mode": "1",
|
|
87
|
+
"cd_env": "dev1",
|
|
88
|
+
"core_param_branch": "master",
|
|
89
|
+
"api_param_branch": "master",
|
|
90
|
+
"api_param_folder": null
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 定制项目
|
|
95
|
+
|
|
96
|
+
指定 `api_param_folder` 后,Jenkins Job 路径变为:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
{folder_name}/dev-后端-core
|
|
100
|
+
{folder_name}/dev-后端-api
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 首次初始化(团队成员)
|
|
104
|
+
|
|
105
|
+
只需一步:配置全局凭证文件。
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# 方式 1:从团队成员拷贝
|
|
109
|
+
cp /path/to/teammate/jenkins-config.json ~/.claude/jenkins-config.json
|
|
110
|
+
|
|
111
|
+
# 方式 2:从模板创建,手动填写凭证
|
|
112
|
+
cp ~/.claude/skills/jenkins-deploy/assets/env_param.template.json ~/.claude/jenkins-config.json
|
|
113
|
+
# 然后替换 __JENKINS_*__ 和 __PORTAINER_*__ 占位符
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### AI 初始化行为
|
|
117
|
+
|
|
118
|
+
当技能被触发但 `jenkins-config.json` 不存在时:
|
|
119
|
+
1. 提示"检测到尚未配置 Jenkins 凭证"
|
|
120
|
+
2. 询问是否从模板创建
|
|
121
|
+
3. 复制模板到 `~/.claude/jenkins-config.json`
|
|
122
|
+
4. 提示用户填写凭证(或拷贝已有配置)
|
|
123
|
+
|
|
124
|
+
## 依赖
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install python-jenkins requests
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## 注意
|
|
131
|
+
|
|
132
|
+
- 本技能用于 dev/test 环境部署,**不涉及生产环境**
|
|
133
|
+
- 如果是 Git 提交/分支管理,请使用 `git-workflow` 技能
|
|
134
|
+
- 如果是代码构建错误排查,请使用 `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,400 @@
|
|
|
1
|
+
# 简单调用jenkins api,构建项目并触发webhook
|
|
2
|
+
import html
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import jenkins
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
# 预设默认值(可修改)
|
|
12
|
+
cd_env = '' # dev或test
|
|
13
|
+
|
|
14
|
+
# dev和test环境独立配置
|
|
15
|
+
jenkins_server_url = 'https://ci.xnzn.net' # jenkins地址
|
|
16
|
+
portainer_server_suffix = '' # portainer服务名后缀
|
|
17
|
+
core_timeout = 60 * 10 # core构建超时时间
|
|
18
|
+
api_timeout = 60 * 5 # api构建超时时间
|
|
19
|
+
portainer_user_name = '' # portainer用户名
|
|
20
|
+
portainer_user_pwd = '' # portainer密码
|
|
21
|
+
portainer_api_url = ''
|
|
22
|
+
portainer_endpoints = ''
|
|
23
|
+
jenkins_user_id = ''
|
|
24
|
+
jenkins_api_token = ''
|
|
25
|
+
core_job = ''
|
|
26
|
+
api_job = ''
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ~~~~~~~~~~~~~~~~~ 配置文件查找 ~~~~~~~~~~~~~~~~~~~~~~~~
|
|
30
|
+
|
|
31
|
+
def find_config_file(filename, search_paths):
|
|
32
|
+
"""按优先级查找配置文件:本地 > 全局"""
|
|
33
|
+
for p in search_paths:
|
|
34
|
+
filepath = os.path.join(p, filename)
|
|
35
|
+
if os.path.exists(filepath):
|
|
36
|
+
return filepath
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# 配置文件搜索路径
|
|
41
|
+
project_dir = os.getcwd()
|
|
42
|
+
home_claude_dir = os.path.join(os.path.expanduser('~'), '.claude')
|
|
43
|
+
|
|
44
|
+
# jenkins-config.json 搜索路径:项目本地 .claude/ > 全局 ~/.claude/
|
|
45
|
+
config_search_paths = [
|
|
46
|
+
os.path.join(project_dir, '.claude'),
|
|
47
|
+
home_claude_dir,
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# last_cd_env.json 搜索路径:项目本地 jenkins/
|
|
51
|
+
state_dir = os.path.join(project_dir, 'jenkins')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# 执行job方法,通过等待时间来判断是否构建成功
|
|
55
|
+
def do_job_by_time(server, job_name, param_branch, param_env, wait_time):
|
|
56
|
+
queue_num = server.build_job(name=job_name,
|
|
57
|
+
parameters={'BRANCH': param_branch, 'VERSION': param_env},
|
|
58
|
+
token=jenkins_api_token)
|
|
59
|
+
print('创建queue... {queue_num}')
|
|
60
|
+
time.sleep(wait_time)
|
|
61
|
+
queue_info = server.get_queue_item(queue_num)
|
|
62
|
+
|
|
63
|
+
print('等待时间到,queue信息:' + str(queue_info))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# 执行job方法,通过请求判断结束,进度更准确
|
|
67
|
+
def do_job(server, job_name, param_branch, param_env, wait_time):
|
|
68
|
+
|
|
69
|
+
queue_num = server.build_job(name=job_name,
|
|
70
|
+
parameters={'BRANCH': param_branch, 'VERSION': param_env},
|
|
71
|
+
token=jenkins_api_token)
|
|
72
|
+
print(f'创建队列... {queue_num}')
|
|
73
|
+
job_number = None
|
|
74
|
+
# 记录开始构建时间
|
|
75
|
+
start_time = time.time()
|
|
76
|
+
|
|
77
|
+
while True:
|
|
78
|
+
time.sleep(10)
|
|
79
|
+
if time.time() - start_time > wait_time:
|
|
80
|
+
print("构建超时,结束")
|
|
81
|
+
exit(1)
|
|
82
|
+
queue_info = server.get_queue_item(queue_num)
|
|
83
|
+
if queue_info['why'] is not None:
|
|
84
|
+
why = queue_info['why']
|
|
85
|
+
print(f'队列等待执行器...{why}')
|
|
86
|
+
elif 'executable' in queue_info:
|
|
87
|
+
job_number = queue_info['executable']['number']
|
|
88
|
+
print(f'开始执行任务...{job_number}')
|
|
89
|
+
break
|
|
90
|
+
else:
|
|
91
|
+
print('执行结束')
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
# 间隔x秒查询构建结果
|
|
95
|
+
while True:
|
|
96
|
+
time.sleep(10)
|
|
97
|
+
|
|
98
|
+
# 如果超过3分钟,直接结束
|
|
99
|
+
if time.time() - start_time > wait_time:
|
|
100
|
+
print("构建超时,结束")
|
|
101
|
+
exit(1)
|
|
102
|
+
headers = {
|
|
103
|
+
"n": str(job_number),
|
|
104
|
+
}
|
|
105
|
+
# 如果job_name包含 '/',则在把'/'替换为/job/
|
|
106
|
+
if '/' in job_name:
|
|
107
|
+
job_name_url = job_name.replace('/', '/job/')
|
|
108
|
+
url = f'{jenkins_server_url}/job/{job_name_url}/buildHistory/ajax'
|
|
109
|
+
else:
|
|
110
|
+
url = f'{jenkins_server_url}/job/{job_name}/buildHistory/ajax'
|
|
111
|
+
|
|
112
|
+
req = requests.Request(method='POST', url=url, headers=headers)
|
|
113
|
+
response = server.jenkins_open(req)
|
|
114
|
+
# 解析response包含的html
|
|
115
|
+
html_str = html.unescape(response)
|
|
116
|
+
if 'Expected build number' in html_str: # 正在等待中
|
|
117
|
+
print(f'\r等待中...', end='')
|
|
118
|
+
continue
|
|
119
|
+
elif ('预计剩余时间' in html_str) or ('Estimated remaining time' in html_str): # 是否包含 预计剩余时间
|
|
120
|
+
try:
|
|
121
|
+
# 取出 '<td style="width:' 和 '%;" class="progress-bar-done"></td>' 之间的字符串
|
|
122
|
+
progress = html_str.split('<td style="width:')[1].split('%;" class="progress-bar-done"></td>')[0]
|
|
123
|
+
# 打印进度更新最后一行
|
|
124
|
+
print(f'\r构建中...{progress}%', end='')
|
|
125
|
+
except IndexError:
|
|
126
|
+
print("无法从HTML字符串中提取进度信息")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
print(f"发生错误: {e}")
|
|
129
|
+
continue
|
|
130
|
+
else:
|
|
131
|
+
print(f"\r构建结束,耗时 {(time.time() - start_time):.2f} 秒", end='\n')
|
|
132
|
+
# 查询构建结果
|
|
133
|
+
build_info = server.get_build_info(job_name, job_number)
|
|
134
|
+
if build_info['result'] == 'SUCCESS':
|
|
135
|
+
print(f"构建成功 {html.unescape(build_info['url'])}")
|
|
136
|
+
else:
|
|
137
|
+
print(f"构建失败({build_info['result']}) {html.unescape(build_info['url'])}")
|
|
138
|
+
print(f"原始 build_info:{build_info}")
|
|
139
|
+
print(f"原始 html:{html_str}")
|
|
140
|
+
return False
|
|
141
|
+
break
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# 获取 Portainer JWT Token
|
|
146
|
+
def get_jwt_token():
|
|
147
|
+
login_url = f"{portainer_api_url}/auth"
|
|
148
|
+
login_data = {
|
|
149
|
+
"Username": portainer_user_name,
|
|
150
|
+
"Password": portainer_user_pwd,
|
|
151
|
+
}
|
|
152
|
+
response = requests.post(login_url, data=json.dumps(login_data))
|
|
153
|
+
token = response.json()["jwt"]
|
|
154
|
+
print('获取token成功')
|
|
155
|
+
return token
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# 获取 Service ID
|
|
159
|
+
def get_server_id(token):
|
|
160
|
+
headers = {
|
|
161
|
+
"Authorization": f"Bearer {token}",
|
|
162
|
+
"Content-Type": "application/json"
|
|
163
|
+
}
|
|
164
|
+
url = portainer_api_url + f'/endpoints/{portainer_endpoints}/docker/services'
|
|
165
|
+
services_response = requests.get(url, headers=headers)
|
|
166
|
+
services = services_response.json()
|
|
167
|
+
|
|
168
|
+
service_name = cd_env + portainer_server_suffix
|
|
169
|
+
# Find service with matching name
|
|
170
|
+
target_service_id = None
|
|
171
|
+
for service in services:
|
|
172
|
+
if service["Spec"]["Name"] == service_name:
|
|
173
|
+
target_service_id = service["ID"]
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
print(f'查询{service_name}的service_id:{target_service_id}')
|
|
177
|
+
return target_service_id
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# 获取 Service 的 WebHook
|
|
181
|
+
def get_service_web_hook():
|
|
182
|
+
pt_token = get_jwt_token()
|
|
183
|
+
service_id = get_server_id(pt_token)
|
|
184
|
+
|
|
185
|
+
headers = {
|
|
186
|
+
"Authorization": f"Bearer {pt_token}",
|
|
187
|
+
"Content-Type": "application/json"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
url = portainer_api_url + f'/webhooks?filters=%7B%22ResourceID%22:%22{service_id}%22,%22EndpointID%22:{portainer_endpoints}%7D'
|
|
191
|
+
services_response = requests.get(url, headers=headers)
|
|
192
|
+
webhook_json = services_response.json()
|
|
193
|
+
target_webhook_token = None
|
|
194
|
+
# 是否存在webhook
|
|
195
|
+
if len(webhook_json) > 0:
|
|
196
|
+
target_webhook_token = webhook_json[0]['Token']
|
|
197
|
+
print('获取webhook_token:' + target_webhook_token)
|
|
198
|
+
|
|
199
|
+
# 尝试调用请求创建webhook
|
|
200
|
+
if target_webhook_token is None:
|
|
201
|
+
payload = {"ResourceID": service_id, "EndpointID": 2, "WebhookType": 1, "registryID": 0}
|
|
202
|
+
services_response = requests.post(portainer_api_url + "/webhooks", headers=headers, data=json.dumps(payload))
|
|
203
|
+
webhook_json = services_response.json()
|
|
204
|
+
if webhook_json is not None and 'Token' in webhook_json:
|
|
205
|
+
target_webhook_token = webhook_json['Token']
|
|
206
|
+
print('创建webhook_token结果:' + target_webhook_token)
|
|
207
|
+
|
|
208
|
+
return target_webhook_token
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# 启动webhook(dev1-15)
|
|
212
|
+
def trigger_webhook():
|
|
213
|
+
|
|
214
|
+
wh_token = get_service_web_hook()
|
|
215
|
+
|
|
216
|
+
if wh_token is None:
|
|
217
|
+
print(f"{cd_env}环境没有配置webhook,结束")
|
|
218
|
+
exit(0)
|
|
219
|
+
else:
|
|
220
|
+
print("开始触发webhook")
|
|
221
|
+
r = requests.post(portainer_api_url + '/webhooks/' + wh_token)
|
|
222
|
+
print("webhook触发结果:" + str(r.status_code))
|
|
223
|
+
|
|
224
|
+
# 通过service id 更新
|
|
225
|
+
def update_portainer():
|
|
226
|
+
|
|
227
|
+
pt_token = get_jwt_token()
|
|
228
|
+
service_id = get_server_id(pt_token)
|
|
229
|
+
|
|
230
|
+
headers = {
|
|
231
|
+
"Authorization": f"Bearer {pt_token}",
|
|
232
|
+
"Content-Type": "application/json"
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# 调用更新(PUT请求):
|
|
236
|
+
url = portainer_api_url + f'/endpoints/{portainer_endpoints}/forceupdateservice'
|
|
237
|
+
payload = {
|
|
238
|
+
"serviceID": service_id,
|
|
239
|
+
"pullImage": True,
|
|
240
|
+
}
|
|
241
|
+
services_response = requests.put(url, headers=headers, data=json.dumps(payload))
|
|
242
|
+
update_rsp_json = services_response.json()
|
|
243
|
+
print('更新结果:' + str(update_rsp_json))
|
|
244
|
+
|
|
245
|
+
# ~~~~~~~~~~~~~~~~~~ 开始输入 ~~~~~~~~~~~~~~~~~~~~~~~
|
|
246
|
+
|
|
247
|
+
# 打印欢迎信息
|
|
248
|
+
print('\n~~~~~~~~~~~~~~')
|
|
249
|
+
print('正在使用jenkins+portainer构建工具,目前支持dev和test环境的自动化构建+更新')
|
|
250
|
+
print('~~~~~~~~~~~~~~\n')
|
|
251
|
+
|
|
252
|
+
# 查找环境配置文件(jenkins-config.json)
|
|
253
|
+
config_file = find_config_file('jenkins-config.json', config_search_paths)
|
|
254
|
+
if config_file is None:
|
|
255
|
+
print('❌ 未找到 jenkins-config.json')
|
|
256
|
+
print(f' 请将配置文件放到以下任一位置:')
|
|
257
|
+
for p in config_search_paths:
|
|
258
|
+
print(f' - {os.path.join(p, "jenkins-config.json")}')
|
|
259
|
+
print(f'\n 或运行: npx ai-engineering-init config --type jenkins')
|
|
260
|
+
exit(1)
|
|
261
|
+
print(f'配置文件: {config_file}')
|
|
262
|
+
|
|
263
|
+
# 确保构建状态目录存在
|
|
264
|
+
os.makedirs(state_dir, exist_ok=True)
|
|
265
|
+
state_file = os.path.join(state_dir, 'last_cd_env.json')
|
|
266
|
+
|
|
267
|
+
# 读取上一次构建的环境(如果不存在则使用默认值)
|
|
268
|
+
if os.path.exists(state_file):
|
|
269
|
+
with open(state_file, 'r', encoding='utf-8') as f:
|
|
270
|
+
cd_env_json = json.load(f)
|
|
271
|
+
else:
|
|
272
|
+
cd_env_json = {
|
|
273
|
+
"build_mode": "1",
|
|
274
|
+
"cd_env": "dev1",
|
|
275
|
+
"core_param_branch": "master",
|
|
276
|
+
"api_param_branch": "master",
|
|
277
|
+
"api_param_folder": None
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# 从终端获取用户输入模式
|
|
281
|
+
build_mode = input(f"请输入模式 0-只构建 1-全构建+更新 2-构建api+更新 3-只更新(预设{cd_env_json['build_mode']}):", ) or cd_env_json['build_mode']
|
|
282
|
+
|
|
283
|
+
# 从终端获取用户输入cd_env
|
|
284
|
+
cd_env = input(f"请输入环境(预设:{cd_env_json['cd_env']}):", ) or cd_env_json['cd_env']
|
|
285
|
+
|
|
286
|
+
# 从终端获取用户输入的分支
|
|
287
|
+
core_param_branch = input(f"请输入core分支(预设:{cd_env_json['core_param_branch']}):") or cd_env_json['core_param_branch']
|
|
288
|
+
|
|
289
|
+
# 从终端输入api分支
|
|
290
|
+
api_param_branch = input(f"请输入api分支(预设:{cd_env_json['api_param_branch']}):") or cd_env_json['api_param_branch']
|
|
291
|
+
|
|
292
|
+
# 从终端输入api文件夹名
|
|
293
|
+
api_param_folder = input(f"请输入定制工程文件夹名(空格或None表示不需要,预设:{cd_env_json['api_param_folder']}):") or cd_env_json['api_param_folder']
|
|
294
|
+
|
|
295
|
+
# 校验模式:
|
|
296
|
+
if build_mode is None or build_mode not in ('0', '1', '2', '3'):
|
|
297
|
+
print("模式错误,结束")
|
|
298
|
+
exit(1)
|
|
299
|
+
# 校验环境,如果不是dev或test开头的,直接退出
|
|
300
|
+
if cd_env is None or not cd_env.startswith('dev') and not cd_env.startswith('test'):
|
|
301
|
+
print("环境错误,结束")
|
|
302
|
+
exit(1)
|
|
303
|
+
# 校验分支
|
|
304
|
+
if core_param_branch is None:
|
|
305
|
+
print("分支错误,结束")
|
|
306
|
+
exit(1)
|
|
307
|
+
if api_param_branch is None:
|
|
308
|
+
print("分支错误,结束")
|
|
309
|
+
exit(1)
|
|
310
|
+
if api_param_folder is None or api_param_folder == '' or api_param_folder == ' ' or api_param_folder == 'none' or api_param_folder == 'None':
|
|
311
|
+
api_param_folder = None
|
|
312
|
+
# 如果dev44及以上环境,只能使用模式0:
|
|
313
|
+
if cd_env.startswith('dev') and int(cd_env[3:]) >= 44 and build_mode != '0':
|
|
314
|
+
print("dev43及以上环境只能使用模式0,结束")
|
|
315
|
+
exit(1)
|
|
316
|
+
|
|
317
|
+
# 保存到本地状态文件
|
|
318
|
+
cd_env_json['cd_env'] = cd_env
|
|
319
|
+
cd_env_json['core_param_branch'] = core_param_branch
|
|
320
|
+
cd_env_json['api_param_branch'] = api_param_branch
|
|
321
|
+
cd_env_json['api_param_folder'] = api_param_folder
|
|
322
|
+
cd_env_json['build_mode'] = build_mode
|
|
323
|
+
json.dump(cd_env_json, open(state_file, 'w', encoding='utf-8'), ensure_ascii=False)
|
|
324
|
+
|
|
325
|
+
# 根据环境输入,设置前缀
|
|
326
|
+
env_prefix = 'dev' if cd_env.startswith('dev') else 'test'
|
|
327
|
+
|
|
328
|
+
# 读取环境配置(从全局或本地 jenkins-config.json)
|
|
329
|
+
with open(config_file, 'r', encoding='utf-8') as f:
|
|
330
|
+
env_param = json.load(f)
|
|
331
|
+
|
|
332
|
+
jenkins_user_id = env_param['jenkins_user_id'][env_prefix]
|
|
333
|
+
jenkins_api_token = env_param['jenkins_api_token'][env_prefix]
|
|
334
|
+
core_job = env_param['core_job'][env_prefix]
|
|
335
|
+
api_job = env_param['api_job'][env_prefix]
|
|
336
|
+
if api_param_folder is not None:
|
|
337
|
+
core_job = api_param_folder + '/' + env_param['core_job_custom'][env_prefix]
|
|
338
|
+
api_job = api_param_folder + '/' + env_param['api_job_custom'][env_prefix]
|
|
339
|
+
|
|
340
|
+
# portainer配置(如果cd_env > dev15,使用另一套配置)
|
|
341
|
+
if cd_env.startswith('dev') and int(cd_env[3:]) > 15:
|
|
342
|
+
portainer_user_name = env_param['portainer_user_name']['dev16']
|
|
343
|
+
portainer_user_pwd = env_param['portainer_user_pwd']['dev16']
|
|
344
|
+
portainer_api_url = env_param['portainer_api_url']['dev16']
|
|
345
|
+
portainer_endpoints = env_param['portainer_endpoints']['dev16']
|
|
346
|
+
portainer_server_suffix = env_param['portainer_server_suffix']['dev16']
|
|
347
|
+
else:
|
|
348
|
+
portainer_api_url = env_param['portainer_api_url'][env_prefix]
|
|
349
|
+
portainer_user_name = env_param['portainer_user_name'][env_prefix]
|
|
350
|
+
portainer_user_pwd = env_param['portainer_user_pwd'][env_prefix]
|
|
351
|
+
portainer_endpoints = env_param['portainer_endpoints'][env_prefix]
|
|
352
|
+
portainer_server_suffix = env_param['portainer_server_suffix'][env_prefix]
|
|
353
|
+
|
|
354
|
+
# ~~~~~~~~~~~~~~~~~ 开始打包 ~~~~~~~~~~~~~~~~~~~~~~~~
|
|
355
|
+
|
|
356
|
+
print('\n~~~~~~~~~~~~~~\n')
|
|
357
|
+
print('开始打包')
|
|
358
|
+
print("链接jenkins:" + jenkins_server_url)
|
|
359
|
+
|
|
360
|
+
# 实例化jenkins对象,连接远程的jenkins master server
|
|
361
|
+
j_server = jenkins.Jenkins(url=jenkins_server_url, username=jenkins_user_id, password=jenkins_api_token)
|
|
362
|
+
print("jenkins信息:" + j_server.get_whoami()['fullName'])
|
|
363
|
+
|
|
364
|
+
# 通过参数构建core
|
|
365
|
+
if int(build_mode) < 2:
|
|
366
|
+
print('开始构建core:' + core_job + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
|
|
367
|
+
rs = do_job(j_server, core_job, core_param_branch, cd_env, core_timeout)
|
|
368
|
+
if rs is False:
|
|
369
|
+
print('构建core失败,结束')
|
|
370
|
+
exit(1)
|
|
371
|
+
time.sleep(10)
|
|
372
|
+
|
|
373
|
+
if int(build_mode) < 3:
|
|
374
|
+
print('开始构建api:' + api_job + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
|
|
375
|
+
rs = do_job(j_server, api_job, api_param_branch, cd_env, api_timeout)
|
|
376
|
+
if rs is False:
|
|
377
|
+
print('构建api失败,结束')
|
|
378
|
+
exit(1)
|
|
379
|
+
time.sleep(2)
|
|
380
|
+
|
|
381
|
+
print('结束打包' + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
|
|
382
|
+
|
|
383
|
+
# ~~~~~~~~~~~~~~~~~ 开始更新 ~~~~~~~~~~~~~~~~~~~~~~~~
|
|
384
|
+
|
|
385
|
+
# 开始更新portainer
|
|
386
|
+
if int(build_mode) == 0:
|
|
387
|
+
print('\n~~~~~~~ 请自行更新portainer ~~~~~~~\n')
|
|
388
|
+
else:
|
|
389
|
+
print('\n~~~~~~~~~~~~~~\n')
|
|
390
|
+
print('开始更新portainer:' + portainer_api_url + ' ' + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
|
|
391
|
+
# portainer配置(如果cd_env > dev15,使用update)
|
|
392
|
+
if cd_env.startswith('dev') and int(cd_env[3:]) > 15:
|
|
393
|
+
# 通过update更新
|
|
394
|
+
update_portainer()
|
|
395
|
+
else:
|
|
396
|
+
# 触发webhook
|
|
397
|
+
trigger_webhook()
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
exit(0)
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
```
|
|
21
|
+
技能目录(随框架更新,不复制到项目):
|
|
22
|
+
~/.claude/skills/jenkins-deploy/assets/jk_build.py ← 构建脚本
|
|
23
|
+
|
|
24
|
+
全局配置(一次性配置,所有项目共享):
|
|
25
|
+
~/.claude/jenkins-config.json ← 凭证(Jenkins/Portainer)
|
|
26
|
+
|
|
27
|
+
项目本地(自动生成):
|
|
28
|
+
jenkins/last_cd_env.json ← 构建状态(环境、分支、模式)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
脚本按 **本地 `.claude/` > 全局 `~/.claude/`** 优先级查找 `jenkins-config.json`。
|
|
32
|
+
|
|
33
|
+
## 构建模式
|
|
34
|
+
|
|
35
|
+
| 模式 | 说明 | 执行步骤 |
|
|
36
|
+
|------|------|---------|
|
|
37
|
+
| `0` | 只构建 | 构建 core + api,不更新 Portainer |
|
|
38
|
+
| `1` | 全构建+更新 | 构建 core + api → 触发 Portainer 更新 |
|
|
39
|
+
| `2` | 构建 api+更新 | 跳过 core,构建 api → 触发 Portainer 更新 |
|
|
40
|
+
| `3` | 只更新 | 不构建,直接触发 Portainer 更新 |
|
|
41
|
+
|
|
42
|
+
## 环境支持
|
|
43
|
+
|
|
44
|
+
| 环境 | 前缀 | Portainer 更新方式 |
|
|
45
|
+
|------|------|-------------------|
|
|
46
|
+
| dev1~15 | `dev` | Webhook 触发 |
|
|
47
|
+
| dev16~43 | `dev` | Force Update(xnzn-dev.xnzn.net) |
|
|
48
|
+
| dev44+ | `dev` | 只支持模式 0(手动更新) |
|
|
49
|
+
| test | `test` | Webhook 触发 |
|
|
50
|
+
|
|
51
|
+
## 使用方式
|
|
52
|
+
|
|
53
|
+
### 运行构建脚本
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python ~/.claude/skills/jenkins-deploy/assets/jk_build.py
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
脚本交互式询问:模式 → 环境 → core 分支 → api 分支 → 定制工程文件夹
|
|
60
|
+
|
|
61
|
+
### 通过 AI 辅助部署
|
|
62
|
+
|
|
63
|
+
当用户说"打包到 devX"时:
|
|
64
|
+
|
|
65
|
+
1. 读取 `jenkins/last_cd_env.json` 获取上次参数
|
|
66
|
+
2. 确认参数:环境、分支、模式
|
|
67
|
+
3. 修改 `jenkins/last_cd_env.json` 写入新参数
|
|
68
|
+
4. 执行 `python ~/.claude/skills/jenkins-deploy/assets/jk_build.py`
|
|
69
|
+
|
|
70
|
+
## 配置文件
|
|
71
|
+
|
|
72
|
+
### jenkins-config.json(凭证,全局)
|
|
73
|
+
|
|
74
|
+
位置:`~/.claude/jenkins-config.json`(或本地 `.claude/jenkins-config.json`)
|
|
75
|
+
|
|
76
|
+
模板:`.claude/skills/jenkins-deploy/assets/env_param.template.json`
|
|
77
|
+
|
|
78
|
+
> **注意**:此文件包含敏感凭证,不要将内容输出到对话中。
|
|
79
|
+
|
|
80
|
+
### last_cd_env.json(构建状态,项目本地)
|
|
81
|
+
|
|
82
|
+
位置:`jenkins/last_cd_env.json`(脚本自动创建,无需手动初始化)
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"build_mode": "1",
|
|
87
|
+
"cd_env": "dev1",
|
|
88
|
+
"core_param_branch": "master",
|
|
89
|
+
"api_param_branch": "master",
|
|
90
|
+
"api_param_folder": null
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 定制项目
|
|
95
|
+
|
|
96
|
+
指定 `api_param_folder` 后,Jenkins Job 路径变为:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
{folder_name}/dev-后端-core
|
|
100
|
+
{folder_name}/dev-后端-api
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 首次初始化(团队成员)
|
|
104
|
+
|
|
105
|
+
只需一步:配置全局凭证文件。
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# 方式 1:从团队成员拷贝
|
|
109
|
+
cp /path/to/teammate/jenkins-config.json ~/.claude/jenkins-config.json
|
|
110
|
+
|
|
111
|
+
# 方式 2:从模板创建,手动填写凭证
|
|
112
|
+
cp ~/.claude/skills/jenkins-deploy/assets/env_param.template.json ~/.claude/jenkins-config.json
|
|
113
|
+
# 然后替换 __JENKINS_*__ 和 __PORTAINER_*__ 占位符
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### AI 初始化行为
|
|
117
|
+
|
|
118
|
+
当技能被触发但 `jenkins-config.json` 不存在时:
|
|
119
|
+
1. 提示"检测到尚未配置 Jenkins 凭证"
|
|
120
|
+
2. 询问是否从模板创建
|
|
121
|
+
3. 复制模板到 `~/.claude/jenkins-config.json`
|
|
122
|
+
4. 提示用户填写凭证(或拷贝已有配置)
|
|
123
|
+
|
|
124
|
+
## 依赖
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install python-jenkins requests
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## 注意
|
|
131
|
+
|
|
132
|
+
- 本技能用于 dev/test 环境部署,**不涉及生产环境**
|
|
133
|
+
- 如果是 Git 提交/分支管理,请使用 `git-workflow` 技能
|
|
134
|
+
- 如果是代码构建错误排查,请使用 `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,134 @@
|
|
|
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
|
+
```
|
|
21
|
+
技能目录(随框架更新,不复制到项目):
|
|
22
|
+
~/.claude/skills/jenkins-deploy/assets/jk_build.py ← 构建脚本
|
|
23
|
+
|
|
24
|
+
全局配置(一次性配置,所有项目共享):
|
|
25
|
+
~/.claude/jenkins-config.json ← 凭证(Jenkins/Portainer)
|
|
26
|
+
|
|
27
|
+
项目本地(自动生成):
|
|
28
|
+
jenkins/last_cd_env.json ← 构建状态(环境、分支、模式)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
脚本按 **本地 `.claude/` > 全局 `~/.claude/`** 优先级查找 `jenkins-config.json`。
|
|
32
|
+
|
|
33
|
+
## 构建模式
|
|
34
|
+
|
|
35
|
+
| 模式 | 说明 | 执行步骤 |
|
|
36
|
+
|------|------|---------|
|
|
37
|
+
| `0` | 只构建 | 构建 core + api,不更新 Portainer |
|
|
38
|
+
| `1` | 全构建+更新 | 构建 core + api → 触发 Portainer 更新 |
|
|
39
|
+
| `2` | 构建 api+更新 | 跳过 core,构建 api → 触发 Portainer 更新 |
|
|
40
|
+
| `3` | 只更新 | 不构建,直接触发 Portainer 更新 |
|
|
41
|
+
|
|
42
|
+
## 环境支持
|
|
43
|
+
|
|
44
|
+
| 环境 | 前缀 | Portainer 更新方式 |
|
|
45
|
+
|------|------|-------------------|
|
|
46
|
+
| dev1~15 | `dev` | Webhook 触发 |
|
|
47
|
+
| dev16~43 | `dev` | Force Update(xnzn-dev.xnzn.net) |
|
|
48
|
+
| dev44+ | `dev` | 只支持模式 0(手动更新) |
|
|
49
|
+
| test | `test` | Webhook 触发 |
|
|
50
|
+
|
|
51
|
+
## 使用方式
|
|
52
|
+
|
|
53
|
+
### 运行构建脚本
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python ~/.claude/skills/jenkins-deploy/assets/jk_build.py
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
脚本交互式询问:模式 → 环境 → core 分支 → api 分支 → 定制工程文件夹
|
|
60
|
+
|
|
61
|
+
### 通过 AI 辅助部署
|
|
62
|
+
|
|
63
|
+
当用户说"打包到 devX"时:
|
|
64
|
+
|
|
65
|
+
1. 读取 `jenkins/last_cd_env.json` 获取上次参数
|
|
66
|
+
2. 确认参数:环境、分支、模式
|
|
67
|
+
3. 修改 `jenkins/last_cd_env.json` 写入新参数
|
|
68
|
+
4. 执行 `python ~/.claude/skills/jenkins-deploy/assets/jk_build.py`
|
|
69
|
+
|
|
70
|
+
## 配置文件
|
|
71
|
+
|
|
72
|
+
### jenkins-config.json(凭证,全局)
|
|
73
|
+
|
|
74
|
+
位置:`~/.claude/jenkins-config.json`(或本地 `.claude/jenkins-config.json`)
|
|
75
|
+
|
|
76
|
+
模板:`.claude/skills/jenkins-deploy/assets/env_param.template.json`
|
|
77
|
+
|
|
78
|
+
> **注意**:此文件包含敏感凭证,不要将内容输出到对话中。
|
|
79
|
+
|
|
80
|
+
### last_cd_env.json(构建状态,项目本地)
|
|
81
|
+
|
|
82
|
+
位置:`jenkins/last_cd_env.json`(脚本自动创建,无需手动初始化)
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"build_mode": "1",
|
|
87
|
+
"cd_env": "dev1",
|
|
88
|
+
"core_param_branch": "master",
|
|
89
|
+
"api_param_branch": "master",
|
|
90
|
+
"api_param_folder": null
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 定制项目
|
|
95
|
+
|
|
96
|
+
指定 `api_param_folder` 后,Jenkins Job 路径变为:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
{folder_name}/dev-后端-core
|
|
100
|
+
{folder_name}/dev-后端-api
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 首次初始化(团队成员)
|
|
104
|
+
|
|
105
|
+
只需一步:配置全局凭证文件。
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# 方式 1:从团队成员拷贝
|
|
109
|
+
cp /path/to/teammate/jenkins-config.json ~/.claude/jenkins-config.json
|
|
110
|
+
|
|
111
|
+
# 方式 2:从模板创建,手动填写凭证
|
|
112
|
+
cp ~/.claude/skills/jenkins-deploy/assets/env_param.template.json ~/.claude/jenkins-config.json
|
|
113
|
+
# 然后替换 __JENKINS_*__ 和 __PORTAINER_*__ 占位符
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### AI 初始化行为
|
|
117
|
+
|
|
118
|
+
当技能被触发但 `jenkins-config.json` 不存在时:
|
|
119
|
+
1. 提示"检测到尚未配置 Jenkins 凭证"
|
|
120
|
+
2. 询问是否从模板创建
|
|
121
|
+
3. 复制模板到 `~/.claude/jenkins-config.json`
|
|
122
|
+
4. 提示用户填写凭证(或拷贝已有配置)
|
|
123
|
+
|
|
124
|
+
## 依赖
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install python-jenkins requests
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## 注意
|
|
131
|
+
|
|
132
|
+
- 本技能用于 dev/test 环境部署,**不涉及生产环境**
|
|
133
|
+
- 如果是 Git 提交/分支管理,请使用 `git-workflow` 技能
|
|
134
|
+
- 如果是代码构建错误排查,请使用 `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,30 @@ 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 homeClaudeDir = path.join(os.homedir(), '.claude');
|
|
595
|
+
const globalConfig = path.join(homeClaudeDir, 'jenkins-config.json');
|
|
596
|
+
const localConfig = path.join(targetDir, '.claude', 'jenkins-config.json');
|
|
597
|
+
const skillAssetsDir = path.join(targetDir, '.claude', 'skills', 'jenkins-deploy', 'assets');
|
|
598
|
+
// 全局安装时也检查全局技能目录
|
|
599
|
+
const globalSkillDir = path.join(homeClaudeDir, 'skills', 'jenkins-deploy', 'assets');
|
|
600
|
+
|
|
601
|
+
const hasSkill = fs.existsSync(skillAssetsDir) || fs.existsSync(globalSkillDir);
|
|
602
|
+
const hasConfig = fs.existsSync(globalConfig) || fs.existsSync(localConfig);
|
|
603
|
+
|
|
604
|
+
// 技能存在但凭证未配置
|
|
605
|
+
if (hasSkill && !hasConfig) {
|
|
606
|
+
console.log(fmt('yellow', fmt('bold', '📦 Jenkins 部署凭证未配置')));
|
|
607
|
+
console.log(` 已安装 ${fmt('bold', 'jenkins-deploy')} 技能,但 ${fmt('bold', 'jenkins-config.json')} 尚未配置。`);
|
|
608
|
+
console.log(` 配置方式:`);
|
|
609
|
+
console.log(` 方式 1:从团队成员处拷贝 ${fmt('bold', '~/.claude/jenkins-config.json')}`);
|
|
610
|
+
console.log(` 方式 2:在 AI 对话中说 ${fmt('bold', '"初始化部署环境"')}`);
|
|
611
|
+
console.log('');
|
|
612
|
+
}
|
|
589
613
|
}
|
|
590
614
|
|
|
591
615
|
function run(selectedTool) {
|
|
@@ -748,6 +772,7 @@ function runUpdate(selectedTool) {
|
|
|
748
772
|
console.log(` 强制更新保留文件: ${fmt('bold', hintCmd('update --force'))}`);
|
|
749
773
|
}
|
|
750
774
|
console.log('');
|
|
775
|
+
showJenkinsHint();
|
|
751
776
|
|
|
752
777
|
if (totalFailed > 0) process.exitCode = 1;
|
|
753
778
|
}
|