@ts-org/jenkins-cli 1.0.0 → 3.0.0
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/README.md +250 -20
- package/dist/cli.d.ts +0 -0
- package/dist/cli.js +33 -9
- package/dist/index.d.ts +40 -2
- package/dist/index.js +21 -6
- package/package.json +31 -14
package/README.md
CHANGED
|
@@ -1,41 +1,271 @@
|
|
|
1
1
|
## @ts-org/jenkins-cli
|
|
2
2
|
|
|
3
|
+
一个轻量的 Jenkins 部署命令行工具:在项目里直接运行 `jenkins-cli`,交互式选择分支和部署环境,然后触发 Jenkins 参数化构建。
|
|
4
|
+
|
|
3
5
|
### 安装
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
|
-
pnpm
|
|
8
|
+
pnpm i -D @ts-org/jenkins-cli
|
|
9
|
+
# or
|
|
10
|
+
npm add -D @ts-org/jenkins-cli
|
|
7
11
|
```
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
Node 版本要求:`>= 16`。
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
### 配置
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
apiToken: https://username:token@jenkins_url
|
|
17
|
+
推荐在项目根目录创建 `jenkins-cli.yaml`:
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
```yaml
|
|
20
|
+
# Jenkins API Token 格式:http(s)://username:token@host:port
|
|
21
|
+
apiToken: http://username:token@jenkins.example.com:8080
|
|
17
22
|
|
|
18
|
-
#
|
|
23
|
+
# Jenkins Job 名称
|
|
24
|
+
job: your-job-name
|
|
19
25
|
|
|
20
|
-
#
|
|
26
|
+
# 部署环境列表(会作为交互选项;触发构建时会传给 Jenkins 参数:mode)
|
|
21
27
|
modes:
|
|
22
28
|
- dev
|
|
23
29
|
- sit
|
|
24
30
|
- uat
|
|
25
31
|
```
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
#### `configs`:扩展交互参数(自动带到 Jenkins)
|
|
34
|
+
|
|
35
|
+
在 `jenkins-cli.yaml` 里加 `configs`,可以在默认交互命令里多问几项(除了 `branch` 和 `mode` 之外),回答会自动作为 Jenkins 参数一起提交。
|
|
36
|
+
|
|
37
|
+
- `type` 基本就是 inquirer 的类型:`input` / `number` / `confirm` / `list` / `rawlist` / `checkbox` / `password`
|
|
38
|
+
- `choices` / `default` / `validate` 支持引用本地 JS/TS 文件的导出:
|
|
39
|
+
- `src/utils/abc.ts:Fun:hello`:取 `hello()` 的返回值(支持 async)
|
|
40
|
+
- `src/utils/abc.ts:Var:hello`:取 `hello` 变量值(若是 Promise 会自动 await)
|
|
41
|
+
- 简写 `src/utils/abc.ts:hello`:等同于 `Var`
|
|
42
|
+
- `name` 不能使用保留字段:`branch` / `modes` / `mode`
|
|
43
|
+
|
|
44
|
+
示例:
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
configs:
|
|
48
|
+
- type: input
|
|
49
|
+
name: version
|
|
50
|
+
message: '请输入版本号:'
|
|
51
|
+
default: '1.0.0'
|
|
52
|
+
|
|
53
|
+
- type: list
|
|
54
|
+
name: service
|
|
55
|
+
message: '请选择服务:'
|
|
56
|
+
choices: src/utils/abc.ts:Fun:services
|
|
57
|
+
|
|
58
|
+
- type: input
|
|
59
|
+
name: ticket
|
|
60
|
+
message: '请输入工单号:'
|
|
61
|
+
validate: src/utils/abc.ts:Fun:validateTicket
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`src/utils/abc.ts` 例子:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
export async function services() {
|
|
68
|
+
return ['service1', 'service2']
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function validateTicket(input: unknown) {
|
|
72
|
+
const s = String(input ?? '').trim()
|
|
73
|
+
if (!s) return 'ticket required'
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
也可以写到 `package.json` 里(适合不想额外放 YAML 的项目):
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"jenkins-cli": {
|
|
83
|
+
"apiToken": "http://username:token@jenkins.example.com:8080",
|
|
84
|
+
"job": "your-job-name",
|
|
85
|
+
"modes": ["dev", "sit", "uat"]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 配置查找优先级
|
|
91
|
+
|
|
92
|
+
从高到低:
|
|
93
|
+
|
|
94
|
+
1. 项目根目录的 `jenkins-cli.yaml`
|
|
95
|
+
2. 项目根目录 `package.json` 里的 `jenkins-cli` 字段
|
|
96
|
+
3. 从项目根的父目录开始向上查找到的 `jenkins-cli.yaml`
|
|
97
|
+
|
|
98
|
+
高优先级会覆盖低优先级的同名字段。
|
|
99
|
+
|
|
100
|
+
### 使用
|
|
101
|
+
|
|
102
|
+
在项目目录执行:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
jenkins-cli
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
也可以在 `package.json` 加个脚本:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"scripts": {
|
|
113
|
+
"ship": "jenkins-cli"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
交互示例:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
🚀 Jenkins CLI - Jenkins Deployment CLI
|
|
122
|
+
|
|
123
|
+
✔ Configuration loaded
|
|
124
|
+
✔ Found 3 branches
|
|
125
|
+
? 请选择要打包的分支: origin/develop
|
|
126
|
+
? 请选择要打包的环境: dev, sit, uat
|
|
127
|
+
|
|
128
|
+
✔ dev - Build triggered successfully
|
|
129
|
+
✔ sit - Build triggered successfully
|
|
130
|
+
✔ uat - Build triggered successfully
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
触发规则补充:
|
|
134
|
+
|
|
135
|
+
- 同一个 `mode` 正在跑的构建,会先尝试停止,再触发新的构建
|
|
136
|
+
- 如果队列里已经有完全一致的任务(`branch` + `mode` 都相同),会直接复用队列项,不重复触发
|
|
137
|
+
|
|
138
|
+
### 非交互触发(trigger)
|
|
139
|
+
|
|
140
|
+
适合脚本/CI 场景:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
jenkins-cli trigger -b origin/develop -m dev
|
|
144
|
+
jenkins-cli trigger -b origin/develop -m dev -m uat
|
|
145
|
+
jenkins-cli trigger -b origin/develop -m dev,uat
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
另外也支持附加任意参数:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
jenkins-cli trigger -b origin/develop -m dev --param foo=bar --param hello=world
|
|
152
|
+
```
|
|
28
153
|
|
|
29
|
-
|
|
154
|
+
### 命令
|
|
155
|
+
|
|
156
|
+
#### builds
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# 最近 N 次构建(默认 20)
|
|
160
|
+
jenkins-cli builds list -n 20
|
|
161
|
+
jenkins-cli builds list -j your-job -n 50
|
|
162
|
+
|
|
163
|
+
# 当前 job 的 running 构建
|
|
164
|
+
jenkins-cli builds running
|
|
165
|
+
jenkins-cli builds running -j your-job
|
|
166
|
+
|
|
167
|
+
# Jenkins 全局 running 构建(不局限 job)
|
|
168
|
+
jenkins-cli builds running -a
|
|
169
|
+
|
|
170
|
+
# 停止指定构建
|
|
171
|
+
jenkins-cli builds stop 123
|
|
172
|
+
|
|
173
|
+
# 先清理该 job 的队列,再停止所有 running
|
|
174
|
+
jenkins-cli builds stop --all
|
|
175
|
+
jenkins-cli builds stop --all -j your-job
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### config
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# 读取 job 配置(默认 xml)
|
|
182
|
+
jenkins-cli config read
|
|
183
|
+
|
|
184
|
+
# 指定 job + 输出 json
|
|
185
|
+
jenkins-cli config read -j your-job -f xml
|
|
186
|
+
jenkins-cli config read -j your-job -f json
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### job
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# 列出所有 job(可按名称过滤)
|
|
193
|
+
jenkins-cli job list
|
|
194
|
+
jenkins-cli job list --search keyword
|
|
195
|
+
# 支持 glob
|
|
196
|
+
jenkins-cli job list --search 'remote*'
|
|
197
|
+
jenkins-cli job list --search remote\\*
|
|
198
|
+
|
|
199
|
+
# 查看 job 信息(可输出原始 JSON)
|
|
200
|
+
jenkins-cli job info
|
|
201
|
+
jenkins-cli job info -j your-job
|
|
202
|
+
jenkins-cli job info -j your-job --json
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### log
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# 查看构建日志
|
|
209
|
+
jenkins-cli log 123
|
|
210
|
+
jenkins-cli log 123 -j your-job
|
|
211
|
+
|
|
212
|
+
# 只看最后 N 行
|
|
213
|
+
jenkins-cli log 123 --tail 200
|
|
214
|
+
|
|
215
|
+
# 持续 follow
|
|
216
|
+
jenkins-cli log 123 -f --interval 1000
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### params
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
# 查看 job 参数定义
|
|
223
|
+
jenkins-cli params
|
|
224
|
+
jenkins-cli params -j your-job
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
#### queue
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# 查看队列(可按 job 过滤)
|
|
231
|
+
jenkins-cli queue list
|
|
232
|
+
jenkins-cli queue list -j your-job
|
|
233
|
+
|
|
234
|
+
# 取消队列项
|
|
235
|
+
jenkins-cli queue cancel 456
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### stop
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
# 停止指定构建
|
|
242
|
+
jenkins-cli stop 123
|
|
243
|
+
jenkins-cli stop 123 -j your-job
|
|
244
|
+
|
|
245
|
+
# 清理该 job 的队列 + 停止该 job 下所有 running
|
|
246
|
+
jenkins-cli stop -a
|
|
247
|
+
jenkins-cli stop -a -j your-job
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### trigger
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
# 触发构建(-m 可重复 / 或用逗号分隔)
|
|
254
|
+
jenkins-cli trigger -b origin/develop -m dev
|
|
255
|
+
jenkins-cli trigger -b origin/develop -m dev,uat
|
|
256
|
+
|
|
257
|
+
# 额外参数
|
|
258
|
+
jenkins-cli trigger -b origin/develop -m dev --param foo=bar
|
|
259
|
+
jenkins-cli trigger -b origin/develop -m dev -j your-job
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### whoami
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# 查看当前 Token 对应用户
|
|
266
|
+
jenkins-cli whoami
|
|
267
|
+
```
|
|
30
268
|
|
|
31
|
-
|
|
32
|
-
🚀 Jenkins CLI - Jenkins Deployment CLI
|
|
269
|
+
### 常见问题
|
|
33
270
|
|
|
34
|
-
|
|
35
|
-
✔ Found 5 branches
|
|
36
|
-
✔ 请选择要打包的分支: origin/develop
|
|
37
|
-
✔ 请选择要打包的环境: dev, sit, uat
|
|
38
|
-
✔ dev - Build triggered successfully
|
|
39
|
-
✔ sit - Build triggered successfully
|
|
40
|
-
✔ uat - Build triggered successfully
|
|
41
|
-
```
|
|
271
|
+
- `stop` / `queue cancel` 遇到 `403 Forbidden`:通常是权限不够(常见需要 `Job -> Cancel`)。如果 Jenkins 开了 CSRF,工具会自动尝试获取 crumb + session cookie;仍失败的话,优先确认你用的是 API Token(不是密码)以及权限配置。
|
package/dist/cli.d.ts
CHANGED
|
File without changes
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {program
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import Ve from 'inquirer';
|
|
4
|
+
import f from 'chalk';
|
|
5
|
+
import De from 'ora';
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import ze, { promisify } from 'util';
|
|
8
|
+
import $ from 'fs-extra';
|
|
9
|
+
import Pe from 'js-yaml';
|
|
10
|
+
import v from 'path';
|
|
11
|
+
import qe from 'axios';
|
|
12
|
+
import Qe from 'jiti';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
15
|
+
|
|
16
|
+
var K="3.0.0",_="Jenkins deployment CLI";var G=promisify(exec);async function H(){try{let{stdout:n}=await G("git branch --show-current"),e=n.trim();if(!e)throw new Error("Not on any branch (detached HEAD state)");return e}catch{throw new Error("Failed to get current branch. Are you in a git repository?")}}async function V(){try{let{stdout:n}=await G('git branch -r --format="%(refname:short)"');return n.split(`
|
|
17
|
+
`).filter(e=>e.includes("/")&&!e.includes("HEAD"))}catch{throw new Error("Failed to list branches. Are you in a git repository?")}}var A="jenkins-cli.yaml",T="package.json",q="jenkins-cli",Ae="jenkinsCli";function z(n,e=process.cwd()){let t=e;for(;;){let r=v.join(t,n);if($.existsSync(r))return r;let o=v.dirname(t);if(o===t)return null;t=o;}}function Be(n=process.cwd()){return z(A,n)}function Ie(n=process.cwd()){let e=z(T,n);return e?v.dirname(e):null}function Le(n){return !!(n.apiToken&&n.job&&Array.isArray(n.modes)&&n.modes.length>0)}async function Y(n){let e=await $.readFile(n,"utf-8"),t=Pe.load(e);if(!t||typeof t!="object")throw new Error(`Invalid config in ${A}: expected a YAML object`);return t}async function Te(n){let e=v.join(n,T);if(!await $.pathExists(e))return {};let t=await $.readFile(e,"utf-8"),r=JSON.parse(t),o=r[q]??r[Ae];if(o==null)return {};if(typeof o!="object"||Array.isArray(o))throw new Error(`Invalid ${T} config: "${q}" must be an object`);return o}async function X(){let n=process.cwd(),e=Ie(n)??n,t=v.join(e,A),r={},o=v.dirname(e),s=Be(o);s&&await $.pathExists(s)&&(r=await Y(s));let i=await Te(e);r={...r,...i};let a=await $.pathExists(t)?await Y(t):{};if(r={...r,...a},!Le(r))throw new Error(`Config incomplete or not found: tried ${A} in project root, "${q}" in $
|
|
18
|
+
{PACKAGE_JSON}, and walking up from cwd. Required: apiToken, job, modes (non-empty array)`);return {...r,projectRoot:e}}var F={maxRedirects:0,validateStatus:n=>n>=200&&n<400};function h(n){return `job/${n.split("/").map(t=>t.trim()).filter(Boolean).map(encodeURIComponent).join("/job/")}`}function U(n){let e=Array.isArray(n)?n.find(o=>o?.parameters):void 0,t=Array.isArray(e?.parameters)?e.parameters:[],r={};for(let o of t)o?.name&&(r[String(o.name)]=o?.value);return r}function Fe(n){try{let t=new URL(n).pathname.split("/").filter(Boolean),r=[];for(let o=0;o<t.length-1;o++)t[o]==="job"&&t[o+1]&&r.push(decodeURIComponent(t[o+1]));return r.join("/")}catch{return ""}}async function Ue(n,e){let r=(await n.axios.get(new URL("api/json?tree=number,url,building,result,timestamp,duration,estimatedDuration,fullDisplayName,displayName,actions[parameters[name,value]]",e).toString())).data??{};return {...r,parameters:U(r.actions)}}function Oe(n,e=400){try{let r=(typeof n=="string"?n:JSON.stringify(n??"",null,2)).trim();return r?r.length>e?`${r.slice(0,e)}...`:r:""}catch{return ""}}function ee(n){let e=n.match(/^(https?):\/\/([^:]+):([^@]+)@(.+)$/);if(!e)throw new Error("Invalid apiToken format. Expected: http(s)://username:token@host:port");let[,t,r,o,s]=e,i=`${t}://${s.replace(/\/$/,"")}/`,a=qe.create({baseURL:i,auth:{username:r,password:o},proxy:!1,timeout:3e4});return {baseURL:i,auth:{username:r,password:o},axios:a,crumbForm:void 0}}async function te(n){try{let e=await n.axios.get("crumbIssuer/api/json"),{data:t}=e;t?.crumbRequestField&&t?.crumb&&(n.axios.defaults.headers.common[t.crumbRequestField]=t.crumb,n.crumbForm={field:t.crumbRequestField,value:t.crumb});let r=e.headers["set-cookie"];if(r){let o=Array.isArray(r)?r.map(s=>s.split(";")[0].trim()):[String(r).split(";")[0].trim()];n.axios.defaults.headers.common.Cookie=o.join("; ");}}catch{}}async function k(n,e){let r=(await n.axios.get("queue/api/json?tree=items[id,task[name],actions[parameters[name,value]],why]")).data.items;if(e){let o=e.trim(),s=o.split("/").filter(Boolean).pop();return r.filter(i=>{let a=i.task?.name;return a===o||(s?a===s:!1)})}return r}async function J(n,e){let t=await n.axios.get(`${h(e)}/api/json?tree=builds[number,url,building,result,timestamp,duration,estimatedDuration,actions[parameters[name,value]]]`);return (Array.isArray(t.data?.builds)?t.data.builds:[]).filter(o=>o?.building).map(o=>({...o,parameters:U(o.actions)}))}async function ne(n){let e=await n.axios.get("computer/api/json?tree=computer[displayName,executors[currentExecutable[number,url]],oneOffExecutors[currentExecutable[number,url]]]"),t=Array.isArray(e.data?.computer)?e.data.computer:[],r=[];for(let i of t){let a=Array.isArray(i?.executors)?i.executors:[],l=Array.isArray(i?.oneOffExecutors)?i.oneOffExecutors:[];for(let u of [...a,...l]){let c=u?.currentExecutable;c?.url&&r.push({number:Number(c.number),url:String(c.url)});}}let o=new Map;for(let i of r)i.url&&o.set(i.url,i);return (await Promise.all([...o.values()].map(async i=>{let a=await Ue(n,i.url),l=Fe(String(a?.url??i.url));return {...a,job:l}}))).filter(i=>i?.building)}async function N(n,e){return await n.axios.post("queue/cancelItem",new URLSearchParams({id:e.toString()}),F),!0}async function C(n,e,t){try{await n.axios.post(`${h(e)}/${t}/stop/`,new URLSearchParams({}),F);}catch(r){let o=r.response?.status;if(o===403){let s=typeof r.response?.data=="string"?r.response.data.slice(0,200):JSON.stringify(r.response?.data??"").slice(0,200),i=/crumb|csrf/i.test(s);console.log(f.red("[Error] 403 Forbidden: Jenkins \u62D2\u7EDD\u505C\u6B62\u6784\u5EFA\u8BF7\u6C42\uFF08\u53EF\u80FD\u662F\u6743\u9650\u6216 CSRF\uFF09\u3002")),console.log(f.yellow('[Hint] \u8BF7\u786E\u8BA4\u5F53\u524D\u7528\u6237\u5BF9\u8BE5 Job \u62E5\u6709 "Job" -> "Cancel" \u6743\u9650\u3002')),console.log(i?"[Hint] Jenkins returned a crumb/CSRF error. Ensure crumb + session cookie are sent, or use API token (not password).":s?`[Hint] Jenkins response: ${s}`:'[Hint] If "Job/Cancel" is already granted, also try granting "Run" -> "Update" (some versions use it for stop).');return}if(o===404||o===400||o===500){console.log(`[Info] Build #${t} could not be stopped (HTTP ${o}). Assuming it finished.`);return}throw r}}async function B(n,e,t){let r="branch",o="mode",s=t[o];s&&console.log(`Checking for conflicting builds for mode: ${s}...`);let[i,a]=await Promise.all([k(n,e),J(n,e)]),l=(m,p)=>{let y=m.actions?.find(E=>E.parameters);return y?y.parameters.find(E=>E.name===p)?.value:void 0},u=t[r];if(s&&u){let m=i.find(p=>l(p,o)===s&&l(p,r)===u);if(m)return console.log(`Build already in queue (ID: ${m.id}). Skipping trigger.`),new URL(`queue/item/${m.id}/`,n.baseURL).toString()}let c=!1;if(s)for(let m of a)l(m,o)===s&&(console.log(`Stopping running build #${m.number} (mode=${s})...`),await C(n,e,m.number),c=!0);c&&await new Promise(m=>setTimeout(m,1e3)),s&&console.log(`Triggering new build for mode=${s}...`);try{return (await n.axios.post(`${h(e)}/buildWithParameters/`,new URLSearchParams({...t}),F)).headers.location||""}catch(m){let p=m?.response?.status,y=Oe(m?.response?.data);if(p===400){let x=[];x.push("Hint: Jenkins returned 400. Common causes: job is not parameterized, parameter names do not match, or the job endpoint differs (e.g. multibranch jobs)."),x.push(`Hint: This CLI sends parameters "${r}" and "${o}". Ensure your Jenkins Job has matching parameter names.`);let E=y?`
|
|
19
|
+
Jenkins response (snippet):
|
|
20
|
+
${y}`:"";throw new Error(`Request failed with status code 400.${E}
|
|
21
|
+
${x.join(`
|
|
22
|
+
`)}`)}if(p){let x=y?`
|
|
23
|
+
Jenkins response (snippet):
|
|
24
|
+
${y}`:"";throw new Error(`Request failed with status code ${p}.${x}`)}throw m}}async function re(n){return (await n.axios.get("whoAmI/api/json")).data}async function oe(n){return (await n.axios.get("api/json?tree=jobs[name,url,color]")).data.jobs??[]}async function ie(n,e){return (await n.axios.get(`${h(e)}/api/json?tree=name,fullName,url,buildable,inQueue,nextBuildNumber,color,healthReport[description,score],lastBuild[number,url,building,result,timestamp,duration],lastCompletedBuild[number,url,result,timestamp,duration]`)).data}async function se(n,e,t){let r=Math.max(1,t?.limit??20);return ((await n.axios.get(`${h(e)}/api/json?tree=builds[number,url,building,result,timestamp,duration,actions[parameters[name,value]]]{0,${r}}`)).data.builds??[]).map(i=>({number:i.number,result:i.result,building:i.building,timestamp:i.timestamp,duration:i.duration,url:i.url,parameters:U(i.actions)}))}async function ae(n,e,t){let r=await n.axios.get(`${h(e)}/${t}/consoleText`,{responseType:"text"});return String(r.data??"")}async function ce(n,e,t,r){let o=Math.max(0,r?.start??0),s=Math.max(200,r?.intervalMs??1e3);for(;;){let i=await n.axios.get(`${h(e)}/${t}/logText/progressiveText?start=${o}`,{responseType:"text"}),a=String(i.data??"");a&&process.stdout.write(a);let l=i.headers["x-text-size"],u=i.headers["x-more-data"],c=l?Number(l):NaN;if(Number.isNaN(c)||(o=c),!(String(u??"").toLowerCase()==="true"))break;await new Promise(p=>setTimeout(p,s));}}async function le(n,e){return ((await n.axios.get(`${h(e)}/api/json?tree=property[parameterDefinitions[name,type,description,defaultParameterValue[value],choices]]`)).data?.property??[]).flatMap(s=>s?.parameterDefinitions??[]).filter(Boolean).map(s=>({name:s.name,type:s.type,description:s.description,default:s.defaultParameterValue?.value,choices:s.choices}))}async function ue(n,e){let t=await n.axios.get(`${h(e)}/config.xml`,{responseType:"text"});return String(t.data??"")}async function g(){let n=De("Loading configuration...").start();try{let e=await X();n.succeed("Configuration loaded");let t=ee(e.apiToken);return await te(t),{config:e,jenkins:t}}catch(e){n.fail(f.red(e.message)),process.exit(1);}}var _e=Qe(fileURLToPath(import.meta.url),{interopDefault:!0});async function O(n){return await Promise.resolve(n)}function Ge(n){let e=String(n??"").trim();if(!e)return null;let t=e.split(":");if(t.length<2)return null;if(t.length===2){let[i,a]=t;return !i||!a?null:{file:i,kind:"Var",exportName:a}}let r=t.at(-2),o=t.at(-1);if(r!=="Fun"&&r!=="Var"||!o)return null;let s=t.slice(0,-2).join(":");return s?{file:s,kind:r,exportName:o}:null}function He(n,e){return v.isAbsolute(e)?e:v.join(n,e)}function pe(n){return !!n&&typeof n=="object"&&!Array.isArray(n)}async function fe(n,e){let t=Ge(e);if(!t)return null;let r=He(n,t.file);if(!await $.pathExists(r))throw new Error(`configs reference not found: ${t.file}`);let o=_e(r),s=pe(o)?o:{default:o},i=t.exportName==="default"?s.default:s[t.exportName];if(i===void 0)throw new Error(`configs reference export not found: ${e}`);return {kind:t.kind,value:i}}async function de(n,e){if(typeof e!="string")return e;let t=await fe(n,e);if(!t)return e;if(t.kind==="Var"){if(typeof t.value=="function"){let o=t.value();return await O(o)}return await O(t.value)}if(typeof t.value!="function")throw new Error(`configs reference expected a function: ${e}`);let r=t.value();return await O(r)}async function ge(n,e){let t=Array.isArray(n)?n:[];if(t.length===0)return [];let r=String(e.projectRoot??"").trim()||process.cwd(),o=[];for(let s of t){if(!pe(s))continue;let i={...s};if(i.choices!==void 0){i.choices=await de(r,i.choices);let a=String(i.type??"").toLowerCase(),l=i.choices;if((a==="list"||a==="rawlist"||a==="checkbox")&&typeof l!="function"&&!Array.isArray(l))if(l==null)i.choices=[];else if(typeof l=="string"||typeof l=="number"||typeof l=="boolean")i.choices=[String(l)];else if(typeof l[Symbol.iterator]=="function")i.choices=Array.from(l).map(c=>String(c));else throw new Error(`configs "${String(i.name??"")}" choices must resolve to an array (got ${typeof l})`)}if(i.default!==void 0&&(i.default=await de(r,i.default)),i.validate!==void 0){let a=i.validate;if(typeof a=="string"){let l=await fe(r,a);if(!l)i.validate=a;else if(l.kind==="Var"){if(typeof l.value!="function")throw new Error(`validate reference must be a function: ${a}`);i.validate=l.value;}else {if(typeof l.value!="function")throw new Error(`validate reference must be a function: ${a}`);i.validate=(u,c)=>l.value(u,c);}}else typeof a=="function"&&(i.validate=a);}o.push(i);}return o}function be(n,e){let t=new Set(e),r={};for(let[o,s]of Object.entries(n))if(!t.has(o)&&s!==void 0){if(s===null){r[o]="";continue}if(Array.isArray(s)){r[o]=s.map(i=>String(i)).join(",");continue}if(typeof s=="object"){try{r[o]=JSON.stringify(s);}catch{r[o]=String(s);}continue}r[o]=String(s);}return r}async function ye(){console.log(f.bold.blue(`
|
|
8
25
|
\u{1F680} Jenkins CLI - Jenkins Deployment CLI
|
|
9
|
-
`));let
|
|
10
|
-
\u{1F44B} Cancelled by user`)),process.exit(
|
|
26
|
+
`));let{config:n,jenkins:e}=await g(),[t,r]=await Promise.all([V(),H()]),o=()=>{console.log(f.yellow(`
|
|
27
|
+
\u{1F44B} Cancelled by user`)),process.exit(130);},s,i={};try{process.prependOnceListener("SIGINT",o);let l=await ge(n.configs,{projectRoot:n.projectRoot}),u=new Set(["branch","modes","mode"]);for(let m of l){let p=String(m?.name??"").trim();if(u.has(p))throw new Error(`configs name "${p}" is reserved. Use another name.`)}let c=await Ve.prompt([{type:"list",name:"branch",message:"\u8BF7\u9009\u62E9\u8981\u6253\u5305\u7684\u5206\u652F:",choices:t,default:t.indexOf(r)},{type:"checkbox",name:"modes",message:"\u8BF7\u9009\u62E9\u8981\u6253\u5305\u7684\u73AF\u5883:",choices:n.modes,validate:m=>m.length===0?"You must select at least one environment":!0},...l]);s={branch:String(c.branch??""),modes:c.modes??[]},i=be(c,["branch","modes"]);}catch(l){let u=l,c=u?.message?String(u.message):String(u);((u?.name?String(u.name):"")==="ExitPromptError"||c.toLowerCase().includes("cancelled")||c.toLowerCase().includes("canceled"))&&(console.log(f.yellow(`
|
|
28
|
+
\u{1F44B} Cancelled by user`)),process.exit(0)),console.log(f.red(`
|
|
29
|
+
\u274C Prompt failed: ${c}
|
|
30
|
+
`)),process.exit(1);}finally{process.off("SIGINT",o);}console.log();let a=s.modes.map(async l=>{let u=De(`Triggering build for ${f.yellow(l)}...`).start();try{let c=await B(e,n.job,{...i,branch:s.branch,mode:l});u.succeed(`${f.green(l)} - Triggered! Queue: ${c}`);}catch(c){throw u.fail(`${f.red(l)} - ${c.message}`),c}});try{await Promise.all(a),console.log();}catch{console.log(f.bold.red(`
|
|
11
31
|
\u274C Some builds failed. Check the output above.
|
|
12
|
-
`)),process.exit(1);}}
|
|
32
|
+
`)),process.exit(1);}}function b(n,e){return (e??"").trim()||n}function w(n,e){let t=Number(n);if(!Number.isInteger(t)||t<0)throw new Error(`${e} must be a non-negative integer`);return t}function we(n){let e=n.command("queue").description("\u961F\u5217\u76F8\u5173\u64CD\u4F5C");e.command("list").description("\u83B7\u53D6\u961F\u5217\u5217\u8868").option("-j, --job <job>","\u4EC5\u663E\u793A\u6307\u5B9A job \u7684\u961F\u5217\u9879\uFF08\u4E0D\u586B\u5219\u663E\u793A\u5168\u90E8\uFF09").action(async t=>{let{config:r,jenkins:o}=await g(),s=t.job?b(r.job,t.job):void 0,i=await k(o,s);console.table((i??[]).map(a=>({id:a.id,job:a.task?.name,why:a.why})));}),e.command("cancel").description("\u53D6\u6D88\u961F\u5217\u9879").argument("<id>").action(async t=>{let{jenkins:r}=await g(),o=await N(r,w(t,"id"));console.log(o?f.green("Cancelled"):f.red("Cancel failed"));});}function d(n){return {[ze.inspect.custom]:()=>n}}function S(n){let e=String(n??""),t=e.trim();if(!t)return d(f.gray("-"));let r=s=>d(s),o=t.toLowerCase();if(o.startsWith("http://")||o.startsWith("https://"))try{let i=(new URL(t).hostname??"").toLowerCase(),a=i==="localhost"||i==="127.0.0.1"||i==="::1",l=/^10\./.test(i)||/^192\.168\./.test(i)||/^127\./.test(i)||/^172\.(1[6-9]|2\d|3[0-1])\./.test(i);return r(a||l?f.cyanBright(e):f.hex("#4EA1FF")(e))}catch{return r(f.hex("#4EA1FF")(e))}return o.startsWith("ws://")||o.startsWith("wss://")?r(f.cyan(e)):o.startsWith("ssh://")||o.startsWith("git+ssh://")||o.startsWith("git@")?r(f.magenta(e)):o.startsWith("file://")?r(f.yellow(e)):o.startsWith("mailto:")?r(f.green(e)):o.startsWith("javascript:")||o.startsWith("data:")?r(f.red(e)):t.startsWith("/")||t.startsWith("./")||t.startsWith("../")?r(f.cyan(e)):/^[a-z0-9.-]+(?::\d+)?(\/|$)/i.test(t)?r(f.blue(e)):r(e)}function R(n){let e=new Date(n),t=r=>String(r).padStart(2,"0");return `${e.getFullYear()}-${t(e.getMonth()+1)}-${t(e.getDate())} ${t(e.getHours())}:${t(e.getMinutes())}:${t(e.getSeconds())}`}function P(n,e){if(e===!0)return d(f.cyan("BUILDING"));let t=String(n??"").toUpperCase();return d(t?t==="SUCCESS"?f.green(t):t==="ABORTED"?f.gray(t):t==="FAILURE"?f.red(t):t==="UNSTABLE"?f.yellow(t):t==="NOT_BUILT"?f.gray(t):t:"-")}function M(n){let e=String(n??"");if(!e)return d("-");let t=e.endsWith("_anime"),r=t?e.slice(0,-6):e,o=(()=>{switch(r){case"blue":return t?f.blueBright(e):f.blue(e);case"red":return t?f.redBright(e):f.red(e);case"yellow":return t?f.yellowBright(e):f.yellow(e);case"aborted":return f.gray(e);case"disabled":case"notbuilt":case"grey":case"gray":return f.gray(e);default:return e}})();return d(o)}function Xe(n){let e=Object.entries(n??{}).filter(([r])=>r);if(!e.length)return d("-");e.sort(([r],[o])=>r.localeCompare(o,"en",{sensitivity:"base"}));let t=e.map(([r,o])=>{if(o===void 0)return `${r}=`;if(o===null)return `${r}=null`;if(typeof o=="string"||typeof o=="number"||typeof o=="boolean")return `${r}=${o}`;try{return `${r}=${JSON.stringify(o)}`}catch{return `${r}=${String(o)}`}}).join(", ");return d(t)}function xe(n){let e=n.command("builds").description("\u6784\u5EFA\u76F8\u5173\u64CD\u4F5C");e.command("list").description("\u83B7\u53D6\u6784\u5EFA\u5217\u8868").option("-j, --job <job>","\u6307\u5B9A job").option("-n, --limit <n>","\u6570\u91CF","20").action(async t=>{let{config:r,jenkins:o}=await g(),s=b(r.job,t.job),i=await se(o,s,{limit:Number(t.limit)});console.table(i.map(a=>({number:a.number,result:P(a.result,a.building),building:a.building,durationS:Number((Number(a.duration??0)/1e3).toFixed(3)),date:d(R(Number(a.timestamp??0)))})));}),e.command("running").description("\u83B7\u53D6\u6B63\u5728\u8FD0\u884C\u7684\u6784\u5EFA").option("-j, --job <job>","\u6307\u5B9A job").option("-a, --all","\u67E5\u8BE2 Jenkins \u4E0B\u6240\u6709\u6B63\u5728\u8FD0\u884C\u7684\u6784\u5EFA\uFF08\u5FFD\u7565 --job\uFF09").action(async t=>{let{config:r,jenkins:o}=await g(),s=t.all?await ne(o):await J(o,b(r.job,t.job));console.table((s??[]).map(i=>({...t.all?{job:d(String(i.job??""))}:{},number:i.number,date:d(i.timestamp?R(Number(i.timestamp)):"-"),parameters:Xe(i.parameters),url:S(String(i.url??""))})));}),e.command("stop").description("\u53D6\u6D88\u961F\u5217\u5E76\u505C\u6B62\u6784\u5EFA").argument("[id]","build number\uFF08\u4E0D\u586B\u5219\u9700\u914D\u5408 --all\uFF09").option("-j, --job <job>","\u6307\u5B9A job").option("-a, --all","\u5148\u53D6\u6D88\u8BE5 job \u7684\u961F\u5217\u9879\uFF0C\u518D\u505C\u6B62\u8BE5 job \u4E0B\u6240\u6709 running \u7684\u6784\u5EFA").action(async(t,r)=>{let{config:o,jenkins:s}=await g(),i=b(o.job,r.job);if(r.all){let[a,l]=await Promise.all([k(s,i),J(s,i)]),u=(a??[]).map(m=>Number(m.id)).filter(m=>!Number.isNaN(m)),c=(l??[]).map(m=>Number(m.number)).filter(m=>!Number.isNaN(m));for(let m of u)await N(s,m);for(let m of c)console.log(`Stopping build #${m}...`),await C(s,i,m);if(!u.length&&!c.length){console.log(f.yellow("No queue items or running builds"));return}console.log(f.green(`Requested: cancelled ${u.length} queue item(s), stopped ${c.length} running build(s).`));return}if(!t){console.log(f.red("Missing build id. Provide <id> or use --all."));return}await C(s,i,w(t,"id")),console.log(f.green("Stop requested"));});}function je(n){n.command("log").description("\u83B7\u53D6\u6784\u5EFA\u63A7\u5236\u53F0\u65E5\u5FD7").argument("<id>").option("-j, --job <job>","\u6307\u5B9A job").option("--tail <n>","\u4EC5\u8F93\u51FA\u6700\u540E N \u884C").option("-f, --follow","\u6301\u7EED\u8F93\u51FA\uFF08\u76F4\u5230 Jenkins \u8868\u793A\u65E0\u66F4\u591A\u65E5\u5FD7\uFF09").option("--interval <ms>","follow \u62C9\u53D6\u95F4\u9694","1000").action(async(e,t)=>{let{config:r,jenkins:o}=await g(),s=b(r.job,t.job),i=w(e,"id");if(t.follow){await ce(o,s,i,{intervalMs:Number(t.interval)});return}let a=await ae(o,s,i);if(t.tail){let l=w(t.tail,"tail"),u=a.split(`
|
|
33
|
+
`);console.log(u.slice(Math.max(0,u.length-l)).join(`
|
|
34
|
+
`));return}process.stdout.write(a);});}function ke(n){n.command("stop").description("\u505C\u6B62\u6B63\u5728\u8FD0\u884C\u7684\u6784\u5EFA / \u6E05\u7406\u961F\u5217").argument("[id]","build number\uFF08\u4E0D\u586B\u5219\u9700\u914D\u5408 -a\uFF09").option("-j, --job <job>","\u6307\u5B9A job").option("-a, --all","\u53D6\u6D88\u8BE5 job \u7684\u961F\u5217\u9879\uFF0C\u5E76\u505C\u6B62\u8BE5 job \u4E0B\u6240\u6709\u6B63\u5728\u8FD0\u884C\u7684\u6784\u5EFA").action(async(e,t)=>{let{config:r,jenkins:o}=await g(),s=b(r.job,t.job);if(t.all){let[i,a]=await Promise.all([k(o,s),J(o,s)]),l=(i??[]).map(c=>Number(c.id)).filter(c=>!Number.isNaN(c)),u=(a??[]).map(c=>Number(c.number)).filter(c=>!Number.isNaN(c));for(let c of l)await N(o,c);for(let c of u)await C(o,s,c);console.log(f.green(`Requested: cancelled ${l.length} queue item(s), stopped ${u.length} running build(s).`));return}if(!e){console.log(f.red("Missing build id. Provide <id> or use -a/--all."));return}await C(o,s,w(e,"id")),console.log(f.green("Stop requested"));});}function Ce(n){n.command("params").description("\u83B7\u53D6 job \u53C2\u6570\u5B9A\u4E49").option("-j, --job <job>","\u6307\u5B9A job").action(async e=>{let{config:t,jenkins:r}=await g(),o=b(t.job,e.job),s=await le(r,o),i=Math.max(0,...s.map(a=>String(a?.name??"").length));console.table((s??[]).map(a=>({name:d(String(a?.name??"").padEnd(i," ")),type:d(String(a?.type??"")),description:d(String(a?.description??"")),default:d(a?.default===void 0?"-":String(a.default)),choices:d(Array.isArray(a?.choices)?a.choices.join(", "):a?.choices??"-")})));});}function Se(n){n.command("whoami").description("\u67E5\u770B\u5F53\u524D Token \u5BF9\u5E94\u7684 Jenkins \u7528\u6237\u4FE1\u606F").action(async()=>{let{jenkins:e}=await g(),t=await re(e),r=t&&typeof t=="object"?t:{value:t},o=(u,c)=>{let m=u.length;if(m>=c)return u;let p=c-m,y=Math.floor(p/2),x=p-y;return `${" ".repeat(y)}${u}${" ".repeat(x)}`},s=r.name??r.fullName??r.id??"",i=s==null?"":String(s),a=Math.max(20,i.length),l={};l.name=d(o(i,a));for(let u of Object.keys(r).sort((c,m)=>c.localeCompare(m,"en"))){if(u==="name")continue;let c=r[u],m=u.toLowerCase();if((m==="url"||m.endsWith("url")||m.includes("url"))&&(typeof c=="string"||typeof c=="number")){l[u]=S(String(c));continue}let y=c==null?"":typeof c=="object"?JSON.stringify(c):String(c);l[u]=d(y);}console.table([l]);});}function $e(n){n.command("config").description("\u8BFB\u53D6 Jenkins Job \u914D\u7F6E").command("read").description("\u8BFB\u53D6 job \u914D\u7F6E").option("-j, --job <job>","\u6307\u5B9A job").option("-f, --format <format>","\u8F93\u51FA\u683C\u5F0F\uFF1Axml \u6216 json","xml").action(async t=>{let{config:r,jenkins:o}=await g(),s=b(r.job,t.job),i=String(t.format??"xml").toLowerCase();if(i!=="xml"&&i!=="json")throw new Error(`Invalid format: ${t.format}. Expected: xml or json`);let a=await ue(o,s);if(i==="xml"){process.stdout.write(a);return}let u=new XMLParser({ignoreAttributes:!1,attributeNamePrefix:"@_"}).parse(a);console.log(JSON.stringify(u,null,2));});}function ve(n){let e=n.command("job").description("Job \u76F8\u5173\u64CD\u4F5C"),t=(r,o)=>{let s=String(o??"").trim();if(!s)return !0;if(!/[*?]/.test(s))return r.includes(s);let l=`^${s.replace(/[$()*+.?[\\\]^{|}]/g,"\\$&").replace(/\\\*/g,".*").replace(/\\\?/g,".")}$`;try{return new RegExp(l).test(r)}catch{return r.includes(s)}};e.command("list").description("\u83B7\u53D6\u6240\u6709 job").option("--search <keyword>","\u6309\u540D\u79F0\u8FC7\u6EE4\uFF08\u652F\u6301 glob\uFF09").action(async r=>{let{jenkins:o}=await g(),s=await oe(o),i=(r.search??"").trim(),a=i?s.filter(c=>t(String(c.name??""),i)):s;a.sort((c,m)=>String(c?.name??"").localeCompare(String(m?.name??""),"en",{sensitivity:"base"}));let l=Math.max(0,...a.map(c=>String(c?.name??"").length)),u=Math.max(0,...a.map(c=>String(c?.url??"").length));console.table((a??[]).map(c=>({name:d(String(c.name??"").padEnd(l," ")),color:M(c.color),url:S(String(c.url??"").padEnd(u," "))})));}),e.command("info").description("\u83B7\u53D6 job \u4FE1\u606F").option("-j, --job <job>","\u6307\u5B9A job").option("--json","\u8F93\u51FA\u539F\u59CB JSON").action(async r=>{let{config:o,jenkins:s}=await g(),i=b(o.job,r.job),a=await ie(s,i);if(r.json){console.log(JSON.stringify(a,null,2));return}let l=a?.lastBuild,u=a?.lastCompletedBuild,c=Array.isArray(a?.healthReport)?a.healthReport:[];console.table([{name:d(String(a?.name??"")),url:S(String(a?.url??"")),color:M(a?.color),buildable:a?.buildable,inQueue:a?.inQueue,nextBuildNumber:a?.nextBuildNumber,healthScore:c[0]?.score??"-",lastBuild:d(l?.number?`#${l.number}`:"-"),lastResult:P(l?.result,l?.building),lastDurationS:l?.duration===void 0?"-":Number((Number(l.duration)/1e3).toFixed(3)),lastDate:d(l?.timestamp?R(Number(l.timestamp)):"-"),lastCompleted:d(u?.number?`#${u.number}`:"-"),lastCompletedResult:P(u?.result,!1),lastCompletedDate:d(u?.timestamp?R(Number(u.timestamp)):"-")}]);});}function tt(n){return n.split(",").map(e=>e.trim()).filter(Boolean)}function nt(n){let e=n.indexOf("=");if(e<=0)throw new Error(`Invalid --param: "${n}". Expected: key=value`);let t=n.slice(0,e).trim(),r=n.slice(e+1).trim();if(!t)throw new Error(`Invalid --param: "${n}". Key is empty`);return {key:t,value:r}}function Je(n){n.command("trigger").description("\u975E\u4EA4\u4E92\u89E6\u53D1 Jenkins \u6784\u5EFA").requiredOption("-b, --branch <branch>","\u5206\u652F\uFF08\u4F8B\u5982 origin/develop\uFF09").option("-m, --mode <mode>","\u90E8\u7F72\u73AF\u5883\uFF08\u53EF\u91CD\u590D\u4F20\u5165\uFF0C\u6216\u7528\u9017\u53F7\u5206\u9694\uFF1A-m dev -m uat / -m dev,uat\uFF09",(e,t)=>t.concat(tt(e)),[]).option("--param <key=value>","\u989D\u5916\u53C2\u6570\uFF08\u53EF\u91CD\u590D\u4F20\u5165\uFF0C\u4F8B\u5982\uFF1A--param foo=bar\uFF09",(e,t)=>t.concat(e),[]).option("-j, --job <job>","\u6307\u5B9A job").action(async e=>{let{config:t,jenkins:r}=await g(),o=b(t.job,e.job),s=String(e.branch??"").trim();if(!s)throw new Error("branch is required");let i=(e.mode??[]).map(String).map(c=>c.trim()).filter(Boolean),a=Array.from(new Set(i));if(a.length===0)throw new Error("mode is required. Example: jenkins-cli trigger -b origin/develop -m dev -m uat");let l={};for(let c of e.param??[]){let{key:m,value:p}=nt(String(c));l[m]=p;}console.log();let u=a.map(async c=>{let m=De(`Triggering build for ${f.yellow(c)}...`).start();try{let p=await B(r,o,{...l,branch:s,mode:c});m.succeed(`${f.green(c)} - Triggered! Queue: ${p}`);}catch(p){throw m.fail(`${f.red(c)} - ${p.message}`),p}});try{await Promise.all(u),console.log();}catch{console.log(f.bold.red(`
|
|
35
|
+
\u274C Some builds failed. Check the output above.
|
|
36
|
+
`)),process.exit(1);}});}function Ne(n){xe(n),$e(n),ve(n),je(n),Ce(n),we(n),Je(n),ke(n),Se(n);}program.name("jenkins-cli").description(_).version(K,"-v, --version").helpOption("-h, --help").action(ye);Ne(program);program.parse();
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,15 @@ interface JenkinsConfig {
|
|
|
4
4
|
apiToken: string;
|
|
5
5
|
job: string;
|
|
6
6
|
modes: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Extra inquirer questions for interactive default command.
|
|
9
|
+
* Loaded from project root `jenkins-cli.yaml` (or package.json config).
|
|
10
|
+
*/
|
|
11
|
+
configs?: ExtraPromptConfig[];
|
|
12
|
+
/**
|
|
13
|
+
* Internal: resolved project root path used to locate `jenkins-cli.yaml`.
|
|
14
|
+
*/
|
|
15
|
+
projectRoot?: string;
|
|
7
16
|
}
|
|
8
17
|
interface BuildParams {
|
|
9
18
|
branch: string;
|
|
@@ -13,6 +22,25 @@ interface UserChoices {
|
|
|
13
22
|
branch: string;
|
|
14
23
|
modes: string[];
|
|
15
24
|
}
|
|
25
|
+
type InquirerType = 'input' | 'number' | 'confirm' | 'list' | 'rawlist' | 'checkbox' | 'password';
|
|
26
|
+
/**
|
|
27
|
+
* YAML-friendly prompt config (subset of inquirer question options).
|
|
28
|
+
*
|
|
29
|
+
* - `choices` can be an array, or a reference string like:
|
|
30
|
+
* - `src/utils/abc.ts:Fun:hello` (use `hello()` return value)
|
|
31
|
+
* - `src/utils/abc.ts:Var:hello` (use exported `hello` value)
|
|
32
|
+
* - `validate` can be a reference string in the same format.
|
|
33
|
+
*/
|
|
34
|
+
interface ExtraPromptConfig {
|
|
35
|
+
type: InquirerType;
|
|
36
|
+
name: string;
|
|
37
|
+
message: string;
|
|
38
|
+
choices?: unknown;
|
|
39
|
+
default?: unknown;
|
|
40
|
+
validate?: unknown;
|
|
41
|
+
/** Pass-through: any other inquirer options */
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
16
44
|
|
|
17
45
|
declare function loadConfig(): Promise<JenkinsConfig>;
|
|
18
46
|
|
|
@@ -26,8 +54,18 @@ interface JenkinsClient {
|
|
|
26
54
|
password: string;
|
|
27
55
|
};
|
|
28
56
|
axios: AxiosInstance;
|
|
57
|
+
/** crumb(部分 Jenkins 端点除 header 外也要求携带) */
|
|
58
|
+
crumbForm?: {
|
|
59
|
+
field: string;
|
|
60
|
+
value: string;
|
|
61
|
+
};
|
|
29
62
|
}
|
|
30
63
|
declare function createJenkinsClient(apiToken: string): JenkinsClient;
|
|
31
|
-
declare function triggerBuild(jenkins: JenkinsClient, job: string, params: BuildParams): Promise<
|
|
64
|
+
declare function triggerBuild(jenkins: JenkinsClient, job: string, params: BuildParams): Promise<string>;
|
|
65
|
+
|
|
66
|
+
declare function resolveExtraQuestions(configs: ExtraPromptConfig[] | undefined, options: {
|
|
67
|
+
projectRoot?: string;
|
|
68
|
+
}): Promise<Record<string, unknown>[]>;
|
|
69
|
+
declare function answersToJenkinsParams(answers: Record<string, unknown>, excludeKeys: string[]): Record<string, string>;
|
|
32
70
|
|
|
33
|
-
export {
|
|
71
|
+
export { BuildParams, JenkinsConfig, UserChoices, answersToJenkinsParams, createJenkinsClient, getAllBranches, getCurrentBranch, loadConfig, resolveExtraQuestions, triggerBuild };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import d from 'fs-extra';
|
|
2
|
+
import B from 'js-yaml';
|
|
3
|
+
import g from 'path';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import K from 'axios';
|
|
7
|
+
import R from 'chalk';
|
|
8
|
+
import Z from 'jiti';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
var h="jenkins-cli.yaml",b="package.json",x="jenkins-cli",N="jenkinsCli";function A(e,n=process.cwd()){let t=n;for(;;){let r=g.join(t,e);if(d.existsSync(r))return r;let o=g.dirname(t);if(o===t)return null;t=o;}}function I(e=process.cwd()){return A(h,e)}function F(e=process.cwd()){let n=A(b,e);return n?g.dirname(n):null}function T(e){return !!(e.apiToken&&e.job&&Array.isArray(e.modes)&&e.modes.length>0)}async function P(e){let n=await d.readFile(e,"utf-8"),t=B.load(n);if(!t||typeof t!="object")throw new Error(`Invalid config in ${h}: expected a YAML object`);return t}async function L(e){let n=g.join(e,b);if(!await d.pathExists(n))return {};let t=await d.readFile(n,"utf-8"),r=JSON.parse(t),o=r[x]??r[N];if(o==null)return {};if(typeof o!="object"||Array.isArray(o))throw new Error(`Invalid ${b} config: "${x}" must be an object`);return o}async function U(){let e=process.cwd(),n=F(e)??e,t=g.join(n,h),r={},o=g.dirname(n),s=I(o);s&&await d.pathExists(s)&&(r=await P(s));let i=await L(n);r={...r,...i};let a=await d.pathExists(t)?await P(t):{};if(r={...r,...a},!T(r))throw new Error(`Config incomplete or not found: tried ${h} in project root, "${x}" in $
|
|
12
|
+
{PACKAGE_JSON}, and walking up from cwd. Required: apiToken, job, modes (non-empty array)`);return {...r,projectRoot:n}}var J=promisify(exec);async function M(){try{let{stdout:e}=await J("git branch --show-current"),n=e.trim();if(!n)throw new Error("Not on any branch (detached HEAD state)");return n}catch{throw new Error("Failed to get current branch. Are you in a git repository?")}}async function D(){try{let{stdout:e}=await J('git branch -r --format="%(refname:short)"');return e.split(`
|
|
13
|
+
`).filter(n=>n.includes("/")&&!n.includes("HEAD"))}catch{throw new Error("Failed to list branches. Are you in a git repository?")}}var $={maxRedirects:0,validateStatus:e=>e>=200&&e<400};function k(e){return `job/${e.split("/").map(t=>t.trim()).filter(Boolean).map(encodeURIComponent).join("/job/")}`}function H(e){let n=Array.isArray(e)?e.find(o=>o?.parameters):void 0,t=Array.isArray(n?.parameters)?n.parameters:[],r={};for(let o of t)o?.name&&(r[String(o.name)]=o?.value);return r}function _(e,n=400){try{let r=(typeof e=="string"?e:JSON.stringify(e??"",null,2)).trim();return r?r.length>n?`${r.slice(0,n)}...`:r:""}catch{return ""}}function V(e){let n=e.match(/^(https?):\/\/([^:]+):([^@]+)@(.+)$/);if(!n)throw new Error("Invalid apiToken format. Expected: http(s)://username:token@host:port");let[,t,r,o,s]=n,i=`${t}://${s.replace(/\/$/,"")}/`,a=K.create({baseURL:i,auth:{username:r,password:o},proxy:!1,timeout:3e4});return {baseURL:i,auth:{username:r,password:o},axios:a,crumbForm:void 0}}async function G(e,n){let r=(await e.axios.get("queue/api/json?tree=items[id,task[name],actions[parameters[name,value]],why]")).data.items;if(n){let o=n.trim(),s=o.split("/").filter(Boolean).pop();return r.filter(i=>{let a=i.task?.name;return a===o||(s?a===s:!1)})}return r}async function Y(e,n){let t=await e.axios.get(`${k(n)}/api/json?tree=builds[number,url,building,result,timestamp,duration,estimatedDuration,actions[parameters[name,value]]]`);return (Array.isArray(t.data?.builds)?t.data.builds:[]).filter(o=>o?.building).map(o=>({...o,parameters:H(o.actions)}))}async function Q(e,n,t){try{await e.axios.post(`${k(n)}/${t}/stop/`,new URLSearchParams({}),$);}catch(r){let o=r.response?.status;if(o===403){let s=typeof r.response?.data=="string"?r.response.data.slice(0,200):JSON.stringify(r.response?.data??"").slice(0,200),i=/crumb|csrf/i.test(s);console.log(R.red("[Error] 403 Forbidden: Jenkins \u62D2\u7EDD\u505C\u6B62\u6784\u5EFA\u8BF7\u6C42\uFF08\u53EF\u80FD\u662F\u6743\u9650\u6216 CSRF\uFF09\u3002")),console.log(R.yellow('[Hint] \u8BF7\u786E\u8BA4\u5F53\u524D\u7528\u6237\u5BF9\u8BE5 Job \u62E5\u6709 "Job" -> "Cancel" \u6743\u9650\u3002')),console.log(i?"[Hint] Jenkins returned a crumb/CSRF error. Ensure crumb + session cookie are sent, or use API token (not password).":s?`[Hint] Jenkins response: ${s}`:'[Hint] If "Job/Cancel" is already granted, also try granting "Run" -> "Update" (some versions use it for stop).');return}if(o===404||o===400||o===500){console.log(`[Info] Build #${t} could not be stopped (HTTP ${o}). Assuming it finished.`);return}throw r}}async function z(e,n,t){return W(e,n,{branch:t.branch,mode:t.mode})}async function W(e,n,t){let r="branch",o="mode",s=t[o];s&&console.log(`Checking for conflicting builds for mode: ${s}...`);let[i,a]=await Promise.all([G(e,n),Y(e,n)]),u=(c,l)=>{let f=c.actions?.find(w=>w.parameters);return f?f.parameters.find(w=>w.name===l)?.value:void 0},y=t[r];if(s&&y){let c=i.find(l=>u(l,o)===s&&u(l,r)===y);if(c)return console.log(`Build already in queue (ID: ${c.id}). Skipping trigger.`),new URL(`queue/item/${c.id}/`,e.baseURL).toString()}let p=!1;if(s)for(let c of a)u(c,o)===s&&(console.log(`Stopping running build #${c.number} (mode=${s})...`),await Q(e,n,c.number),p=!0);p&&await new Promise(c=>setTimeout(c,1e3)),s&&console.log(`Triggering new build for mode=${s}...`);try{return (await e.axios.post(`${k(n)}/buildWithParameters/`,new URLSearchParams({...t}),$)).headers.location||""}catch(c){let l=c?.response?.status,f=_(c?.response?.data);if(l===400){let m=[];m.push("Hint: Jenkins returned 400. Common causes: job is not parameterized, parameter names do not match, or the job endpoint differs (e.g. multibranch jobs)."),m.push(`Hint: This CLI sends parameters "${r}" and "${o}". Ensure your Jenkins Job has matching parameter names.`);let w=f?`
|
|
14
|
+
Jenkins response (snippet):
|
|
15
|
+
${f}`:"";throw new Error(`Request failed with status code 400.${w}
|
|
16
|
+
${m.join(`
|
|
17
|
+
`)}`)}if(l){let m=f?`
|
|
18
|
+
Jenkins response (snippet):
|
|
19
|
+
${f}`:"";throw new Error(`Request failed with status code ${l}.${m}`)}throw c}}var ne=Z(fileURLToPath(import.meta.url),{interopDefault:!0});async function C(e){return await Promise.resolve(e)}function te(e){let n=String(e??"").trim();if(!n)return null;let t=n.split(":");if(t.length<2)return null;if(t.length===2){let[i,a]=t;return !i||!a?null:{file:i,kind:"Var",exportName:a}}let r=t.at(-2),o=t.at(-1);if(r!=="Fun"&&r!=="Var"||!o)return null;let s=t.slice(0,-2).join(":");return s?{file:s,kind:r,exportName:o}:null}function re(e,n){return g.isAbsolute(n)?n:g.join(e,n)}function j(e){return !!e&&typeof e=="object"&&!Array.isArray(e)}async function S(e,n){let t=te(n);if(!t)return null;let r=re(e,t.file);if(!await d.pathExists(r))throw new Error(`configs reference not found: ${t.file}`);let o=ne(r),s=j(o)?o:{default:o},i=t.exportName==="default"?s.default:s[t.exportName];if(i===void 0)throw new Error(`configs reference export not found: ${n}`);return {kind:t.kind,value:i}}async function v(e,n){if(typeof n!="string")return n;let t=await S(e,n);if(!t)return n;if(t.kind==="Var"){if(typeof t.value=="function"){let o=t.value();return await C(o)}return await C(t.value)}if(typeof t.value!="function")throw new Error(`configs reference expected a function: ${n}`);let r=t.value();return await C(r)}async function oe(e,n){let t=Array.isArray(e)?e:[];if(t.length===0)return [];let r=String(n.projectRoot??"").trim()||process.cwd(),o=[];for(let s of t){if(!j(s))continue;let i={...s};if(i.choices!==void 0){i.choices=await v(r,i.choices);let a=String(i.type??"").toLowerCase(),u=i.choices;if((a==="list"||a==="rawlist"||a==="checkbox")&&typeof u!="function"&&!Array.isArray(u))if(u==null)i.choices=[];else if(typeof u=="string"||typeof u=="number"||typeof u=="boolean")i.choices=[String(u)];else if(typeof u[Symbol.iterator]=="function")i.choices=Array.from(u).map(p=>String(p));else throw new Error(`configs "${String(i.name??"")}" choices must resolve to an array (got ${typeof u})`)}if(i.default!==void 0&&(i.default=await v(r,i.default)),i.validate!==void 0){let a=i.validate;if(typeof a=="string"){let u=await S(r,a);if(!u)i.validate=a;else if(u.kind==="Var"){if(typeof u.value!="function")throw new Error(`validate reference must be a function: ${a}`);i.validate=u.value;}else {if(typeof u.value!="function")throw new Error(`validate reference must be a function: ${a}`);i.validate=(y,p)=>u.value(y,p);}}else typeof a=="function"&&(i.validate=a);}o.push(i);}return o}function se(e,n){let t=new Set(n),r={};for(let[o,s]of Object.entries(e))if(!t.has(o)&&s!==void 0){if(s===null){r[o]="";continue}if(Array.isArray(s)){r[o]=s.map(i=>String(i)).join(",");continue}if(typeof s=="object"){try{r[o]=JSON.stringify(s);}catch{r[o]=String(s);}continue}r[o]=String(s);}return r}
|
|
20
|
+
|
|
21
|
+
export { se as answersToJenkinsParams, V as createJenkinsClient, D as getAllBranches, M as getCurrentBranch, U as loadConfig, oe as resolveExtraQuestions, z as triggerBuild };
|
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ts-org/jenkins-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Jenkins deployment CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"jenkins-cli": "
|
|
8
|
+
"jenkins-cli": "dist/cli.js",
|
|
9
|
+
"jc": "dist/cli.js"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
12
|
"build": "tsup",
|
|
12
13
|
"dev": "NODE_ENV=development tsup --watch",
|
|
13
|
-
"prepublishOnly": "
|
|
14
|
-
"link:test": "
|
|
14
|
+
"prepublishOnly": "npm run build",
|
|
15
|
+
"link:test": "npm run build && npm link",
|
|
16
|
+
"format": "prettier . --write",
|
|
17
|
+
"format:check": "prettier . --check",
|
|
18
|
+
"lint": "eslint .",
|
|
19
|
+
"lint:fix": "eslint . --fix"
|
|
15
20
|
},
|
|
16
21
|
"keywords": [
|
|
17
22
|
"jenkins",
|
|
@@ -19,7 +24,7 @@
|
|
|
19
24
|
"deployment",
|
|
20
25
|
"automation"
|
|
21
26
|
],
|
|
22
|
-
"author": "Lynn<zmzchn@gmail.com>",
|
|
27
|
+
"author": "Lynn <zmzchn@gmail.com>",
|
|
23
28
|
"license": "MIT",
|
|
24
29
|
"repository": {
|
|
25
30
|
"type": "git",
|
|
@@ -32,25 +37,37 @@
|
|
|
32
37
|
"LICENSE"
|
|
33
38
|
],
|
|
34
39
|
"dependencies": {
|
|
35
|
-
"axios": "^1.13.
|
|
40
|
+
"axios": "^1.13.4",
|
|
36
41
|
"chalk": "^5.6.2",
|
|
37
|
-
"commander": "
|
|
42
|
+
"commander": "11.1.0",
|
|
43
|
+
"fast-xml-parser": "^5.3.4",
|
|
38
44
|
"fs-extra": "^11.3.3",
|
|
39
|
-
"inquirer": "
|
|
45
|
+
"inquirer": "9.2.13",
|
|
46
|
+
"jiti": "^2.5.1",
|
|
40
47
|
"js-yaml": "^4.1.1",
|
|
41
|
-
"ora": "^
|
|
48
|
+
"ora": "^7.0.1"
|
|
42
49
|
},
|
|
43
50
|
"devDependencies": {
|
|
44
51
|
"@types/fs-extra": "^11.0.4",
|
|
52
|
+
"@types/inquirer": "^9.0.9",
|
|
45
53
|
"@types/js-yaml": "^4.0.9",
|
|
46
|
-
"@types/node": "^
|
|
47
|
-
"
|
|
48
|
-
"typescript": "^
|
|
54
|
+
"@types/node": "^16.18.126",
|
|
55
|
+
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
56
|
+
"@typescript-eslint/parser": "^6.21.0",
|
|
57
|
+
"eslint": "^8.57.0",
|
|
58
|
+
"eslint-config-prettier": "^9.1.0",
|
|
59
|
+
"prettier": "^3.2.5",
|
|
60
|
+
"tsup": "^6.7.0",
|
|
61
|
+
"typescript": "5.3.3"
|
|
49
62
|
},
|
|
50
63
|
"engines": {
|
|
51
|
-
"node": ">=16
|
|
64
|
+
"node": ">=16"
|
|
52
65
|
},
|
|
53
66
|
"publishConfig": {
|
|
54
67
|
"access": "public"
|
|
55
|
-
}
|
|
68
|
+
},
|
|
69
|
+
"directories": {
|
|
70
|
+
"doc": "docs"
|
|
71
|
+
},
|
|
72
|
+
"types": "./dist/index.d.ts"
|
|
56
73
|
}
|