appship 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +164 -18
- data/lib/appship/builder.rb +48 -39
- data/lib/appship/cache.rb +55 -19
- data/lib/appship/cli.rb +143 -50
- data/lib/appship/config.rb +18 -2
- data/lib/appship/default_message +9 -0
- data/lib/appship/message.rb +193 -0
- data/lib/appship/project_config.rb +115 -0
- data/lib/appship/uploader.rb +26 -10
- data/lib/appship/version.rb +1 -1
- data/lib/appship/webhook.rb +151 -0
- data/lib/appship.rb +3 -0
- data/test/test_cache.rb +263 -0
- data/test/test_cli.rb +356 -0
- data/test/test_display.rb +120 -0
- data/test/test_message.rb +138 -0
- data/test/test_project_config.rb +221 -0
- data/test/test_upload_progress.rb +110 -0
- data/test/test_webhook.rb +352 -0
- metadata +13 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 905aafaf87595c7b9f88071620d883fe4d15e513fc297d55208e88d3fe9fc263
|
|
4
|
+
data.tar.gz: 10b04c144cb0de4c4db92373d4b4b7e5fbc9fc8d25af4c5de2e6e9eb8c9db051
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f362de8ab1337aa62297ddd595c8912dba9b78fd8fa0fec15d2f22a4b8fbefade601bee257155a31a517a404a85789946785a2dd1b89255ad9f191cb8972eb4
|
|
7
|
+
data.tar.gz: 0f44e337fb7f5ac586e506d55a1ac1360b91ccf999f5a5551be781706537fb29e8956f6ceb53bb40e8552ce613dcfea5f8e5004513de453f7a53375bea3adc05
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Appship 是一款面向 iOS 开发者的命令行自动化工具,旨在简化
|
|
|
4
4
|
|
|
5
5
|
它支持自动构建和导出 IPA,并将应用上传至蒲公英(Pgyer)或 fir.im,同时提供 API Key、访问密码和项目配置管理。上传完成后,终端会输出应用版本、密码状态及下载链接,方便团队快速进行测试和分发。
|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### 构建耗时对比(构建机器: Apple M4 16gb)
|
|
8
8
|
|
|
9
9
|
```mermaid
|
|
10
10
|
gantt
|
|
@@ -20,12 +20,15 @@ gantt
|
|
|
20
20
|
|
|
21
21
|
## 安装
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
gem install appship
|
|
23
|
+
macOS 默认的 zsh 终端只需执行以下两条命令:
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
```bash
|
|
26
|
+
gem install appship
|
|
27
|
+
echo 'export PATH="$(ruby -rrubygems -e "print Gem.bindir"):$PATH"' >> ~/.zshrc && source ~/.zshrc
|
|
27
28
|
```
|
|
28
29
|
|
|
30
|
+
第二条命令只需执行一次:将 gem 的命令目录写入 `~/.zshrc`,并立即加载。以后重新打开终端,即可直接执行 `appship pgyer` 或 `appship fir`。
|
|
31
|
+
|
|
29
32
|
### 环境依赖
|
|
30
33
|
|
|
31
34
|
如果需要图标角标,还需要 ImageMagick:
|
|
@@ -50,25 +53,168 @@ appship pgyer
|
|
|
50
53
|
appship fir
|
|
51
54
|
```
|
|
52
55
|
|
|
56
|
+
按终端提示,配置构建信息
|
|
57
|
+
|
|
53
58
|
等待片刻...
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
=======================================================
|
|
59
|
-
分发平台: fir.im
|
|
60
|
+
发布成功后,终端会输出发布消息(默认模板格式):
|
|
61
|
+
|
|
62
|
+
```text
|
|
60
63
|
应用名称: Demo
|
|
61
64
|
应用类型: iOS
|
|
62
65
|
版本信息: 1.0.0(42)
|
|
66
|
+
构建模式: Release
|
|
67
|
+
应用大小: 167.8MB
|
|
68
|
+
更新时间: 2026-11-04 16:52:16
|
|
63
69
|
更新描述: xxx
|
|
64
|
-
|
|
65
|
-
下载链接: https://
|
|
66
|
-
|
|
70
|
+
安装密码: xxx
|
|
71
|
+
下载链接: https://www.pgyer.com/xxxx
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 高阶用法
|
|
75
|
+
|
|
76
|
+
在包含 `.xcodeproj` 的工程根目录执行:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
appship init
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
生成与 `.xcodeproj` 同级的目录和两个示例文件(已有文件不会覆盖):
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
Demo.xcodeproj/
|
|
86
|
+
appship/
|
|
87
|
+
config.yml.sample
|
|
88
|
+
message.yml.sample
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
示例文件不会参与运行。按需复制,去掉 `.sample` 后编辑:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
cp -n appship/config.yml.sample appship/config.yml
|
|
95
|
+
cp -n appship/message.yml.sample appship/message.yml
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`config.yml.sample` 从 `.xcodeproj` 识别 `project_name`,并用注释列出蒲公英、fir.im 和两个机器人的配置字段,不复制全局密钥。生效的 `appship/config.yml` 是 YAML,可只填需要覆盖的字段。例如:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
project_name: Demo
|
|
102
|
+
app_name: DemoApp
|
|
103
|
+
pgyer_password: ""
|
|
104
|
+
wechat_webhook: ""
|
|
105
|
+
...
|
|
67
106
|
```
|
|
68
107
|
|
|
69
|
-
|
|
108
|
+
参数优先级为:命令行参数 → 项目 `appship/config.yml` → `~/.appship/.config` 中同名项目 → 默认值。缺少的字段按字段补齐,不会使用其他项目的配置;项目配置中的空字符串也是明确的值,例如空密码表示不设密码,空 Webhook 表示关闭通知。想继承全局值时,应省略该字段,而非填空字符串。
|
|
109
|
+
|
|
110
|
+
### App Icon 角标
|
|
111
|
+
|
|
112
|
+
在 `appship/config.yml` 中可按需设置:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
app_icon_badge: true
|
|
116
|
+
app_icon_badge_environment: true
|
|
117
|
+
app_icon_badge_version: false # 预留,目前不绘制版本号
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`app_icon_badge: false` 关闭所有角标;启用时默认显示 `DEBUG` 或 `RELEASE`,缓存模式以实际找到的包为准。
|
|
121
|
+
`app_icon_badge_environment: false` 只关闭自动环境文字;已有的自定义 `badge` 仍可用。
|
|
122
|
+
两个开关都不填时,保留原来的选择和默认行为。
|
|
123
|
+
|
|
124
|
+
## 自定义发布消息
|
|
125
|
+
|
|
126
|
+
直接编辑项目下的 `appship/message.yml`,目前支持企微、钉钉、飞书机器人通知,配置多个则会发多条消息通知,格式配置如下:
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
cli_content: |
|
|
130
|
+
应用名称:%{name}
|
|
131
|
+
版本:%{version_info}
|
|
132
|
+
下载:%{url}
|
|
133
|
+
wechat_webhook_success_content: |
|
|
134
|
+
**%{name} 发布成功**
|
|
135
|
+
[下载应用](%{url})
|
|
136
|
+
wechat_webhook_format: markdown
|
|
137
|
+
wechat_webhook_failure_content: |
|
|
138
|
+
❌ %{name} 发布失败(%{configuration})
|
|
139
|
+
原因:%{error_message}
|
|
140
|
+
dingtalk_webhook_success_content: ""
|
|
141
|
+
dingtalk_webhook_failure_content: ""
|
|
142
|
+
feishu_webhook_success_content: |
|
|
143
|
+
%{name} 发布成功
|
|
144
|
+
下载:%{url}
|
|
145
|
+
feishu_webhook_failure_content: ""
|
|
146
|
+
feishu_webhook_format: post
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### message.yml可读参数如下:
|
|
150
|
+
|
|
151
|
+
###### 发布成功
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
name : 应用名称
|
|
155
|
+
build_type_name : 应用类型,例如 iOS、Android
|
|
156
|
+
version_info : 版本信息,例如 1.0.0(42)
|
|
157
|
+
configuration : 构建配置,例如 Debug、Release
|
|
158
|
+
build_size_mb : 应用大小,例如 39.5MB
|
|
159
|
+
build_updated : 更新时间
|
|
160
|
+
description : 本次更新内容
|
|
161
|
+
password : 安装密码或密码设置状态
|
|
162
|
+
url : 应用下载链接
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
###### 失败
|
|
166
|
+
|
|
167
|
+
以下变量用于各机器人的 `*_webhook_failure_content`,在文案中写成 `%{变量名}`。
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
name : 应用名称,缺失时使用工程名;两者都未取得时显示「未知」
|
|
171
|
+
provider : 分发平台,例如「蒲公英」或「fir.im」
|
|
172
|
+
configuration : 构建配置,例如 Debug、Release;未取得时显示「未选择」
|
|
173
|
+
error_type : 异常类型,例如 Appship::CommandError
|
|
174
|
+
error_message : 具体错误说明,例如「归档失败」或「上传失败」
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
例如,在 `appship/message.yml` 中自定义飞书失败文案:
|
|
178
|
+
|
|
179
|
+
```yaml
|
|
180
|
+
feishu_webhook_failure_content: |
|
|
181
|
+
❌ %{name} 发布失败(%{provider} / %{configuration})
|
|
182
|
+
错误类型:%{error_type}
|
|
183
|
+
失败原因:%{error_message}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
失败文案不填写或无效时使用内置文案。失败通知以纯文本发送,发送前会隐藏已知的 API Key、密码、Webhook 地址和 URL 查询参数。
|
|
187
|
+
|
|
188
|
+
[企微 Markdown 配置](https://cloud.tencent.com/document/product/248/50413)
|
|
189
|
+
[飞书自定义机器人](https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot)
|
|
190
|
+
[钉钉群自定义机器人说明](https://open-dingtalk.github.io/developerpedia/docs/learn/bot/webhook/overview/)
|
|
191
|
+
|
|
192
|
+
钉钉存量自定义机器人需配置匹配消息内容的关键词或出口 IP 白名单;Appship 不支持钉钉加签。Webhook 地址不会回显到终端,配置文件权限保持为 `0600`。
|
|
193
|
+
接口参考:
|
|
194
|
+
[企业微信文本消息示例](https://cloud.tencent.com/document/product/1831/134518)
|
|
195
|
+
[钉钉机器人安全设置](https://open.dingtalk.com/document/robots/customize-robot-security-settings)
|
|
196
|
+
|
|
197
|
+
## 本地配置命令
|
|
198
|
+
|
|
199
|
+
#### 通知机器人
|
|
200
|
+
|
|
201
|
+
`wechat_webhook`(企业微信)、`dingtalk_webhook`(钉钉)和 `feishu_webhook`(飞书)都是可选字段,不填写或留空时不发送通知。在项目目录执行以下命令保存地址;URL 需要用引号包裹:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
appship webhook wechat "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx"
|
|
205
|
+
appship webhook dingtalk "https://oapi.dingtalk.com/robot/send?access_token=xxx"
|
|
206
|
+
appship webhook feishu "https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### 关闭某个平台的通知:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
appship webhook wechat ""
|
|
213
|
+
appship webhook dingtalk ""
|
|
214
|
+
appship webhook feishu ""
|
|
215
|
+
```
|
|
70
216
|
|
|
71
|
-
更换安装密码
|
|
217
|
+
#### 更换安装密码
|
|
72
218
|
|
|
73
219
|
```
|
|
74
220
|
appship pgyer -password xxx
|
|
@@ -76,7 +222,7 @@ appship pgyer -password xxx
|
|
|
76
222
|
appship fir -password xxx
|
|
77
223
|
```
|
|
78
224
|
|
|
79
|
-
更换上传秘钥
|
|
225
|
+
#### 更换上传秘钥
|
|
80
226
|
|
|
81
227
|
```
|
|
82
228
|
appship pgyer -key xxx
|
|
@@ -87,8 +233,8 @@ appship fir -key xxx
|
|
|
87
233
|
## 待办
|
|
88
234
|
|
|
89
235
|
```
|
|
90
|
-
1
|
|
91
|
-
2
|
|
92
|
-
3
|
|
236
|
+
1、其他分发平台
|
|
237
|
+
2、AI 接入git log ,自动归纳更新描述
|
|
238
|
+
3、优化命令
|
|
93
239
|
...
|
|
94
240
|
```
|
data/lib/appship/builder.rb
CHANGED
|
@@ -9,6 +9,7 @@ module Appship
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def build!(options)
|
|
12
|
+
templates = Message.templates if options[:upload]
|
|
12
13
|
target = target_options(options)
|
|
13
14
|
scheme = required!(options[:scheme], "--scheme 或配置 scheme")
|
|
14
15
|
configuration = options[:configuration] || "Debug"
|
|
@@ -58,7 +59,7 @@ module Appship
|
|
|
58
59
|
end
|
|
59
60
|
puts "✅ IPA 已生成: #{output}"
|
|
60
61
|
|
|
61
|
-
upload_if_requested!(output, options)
|
|
62
|
+
upload_if_requested!(output, options, templates)
|
|
62
63
|
output
|
|
63
64
|
ensure
|
|
64
65
|
FileUtils.rm_rf(temp_root) if temp_root && !options[:keep_temporary]
|
|
@@ -99,6 +100,7 @@ module Appship
|
|
|
99
100
|
end
|
|
100
101
|
|
|
101
102
|
def package!(options)
|
|
103
|
+
templates = Message.templates if options[:upload]
|
|
102
104
|
app = File.expand_path(required!(options[:app], "--app"))
|
|
103
105
|
raise ConfigurationError, ".app 不存在: #{app}" unless File.directory?(app)
|
|
104
106
|
|
|
@@ -107,17 +109,16 @@ module Appship
|
|
|
107
109
|
runner.require_command!("unzip", "macOS 通常自带 unzip") if options[:badge]
|
|
108
110
|
Ipa.package!(app, output: output, badge: options[:badge], assets: options[:assets], app_icon: options[:app_icon], runner: runner)
|
|
109
111
|
puts "✅ IPA 已生成: #{output}"
|
|
110
|
-
upload_if_requested!(output, options)
|
|
112
|
+
upload_if_requested!(output, options, templates)
|
|
111
113
|
output
|
|
112
114
|
end
|
|
113
115
|
|
|
114
116
|
def upload!(options)
|
|
117
|
+
templates = Message.templates
|
|
115
118
|
file = File.expand_path(required!(options[:file], "IPA 文件路径"))
|
|
116
119
|
raise ConfigurationError, "文件不存在: #{file}" unless File.file?(file)
|
|
117
120
|
|
|
118
|
-
|
|
119
|
-
Display.upload_result(result, file: file, options: options)
|
|
120
|
-
result
|
|
121
|
+
upload_file!(file, options, templates)
|
|
121
122
|
end
|
|
122
123
|
|
|
123
124
|
private
|
|
@@ -191,11 +192,17 @@ module Appship
|
|
|
191
192
|
PLIST
|
|
192
193
|
end
|
|
193
194
|
|
|
194
|
-
def upload_if_requested!(file, options)
|
|
195
|
+
def upload_if_requested!(file, options, templates = nil)
|
|
195
196
|
return unless options[:upload]
|
|
196
197
|
|
|
198
|
+
upload_file!(file, options, templates || Message.templates)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def upload_file!(file, options, templates)
|
|
197
202
|
result = Uploader.new(options).upload!(file)
|
|
198
|
-
Display.upload_result(result, file: file, options: options)
|
|
203
|
+
messages = Display.upload_result(result, file: file, options: options, templates: templates)
|
|
204
|
+
Webhook.new(options).notify(messages, formats: templates.slice(*Message::FORMATS.keys))
|
|
205
|
+
result
|
|
199
206
|
end
|
|
200
207
|
|
|
201
208
|
def required!(value, name)
|
|
@@ -212,12 +219,18 @@ module Appship
|
|
|
212
219
|
module Display
|
|
213
220
|
module_function
|
|
214
221
|
|
|
215
|
-
def upload_result(result, file:, options: {})
|
|
216
|
-
if result.is_a?(Hash) && result["provider"] == "fir"
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
def upload_result(result, file:, options: {}, templates: Message.templates)
|
|
223
|
+
values = if result.is_a?(Hash) && result["provider"] == "fir"
|
|
224
|
+
fir_message_values(result, file: file, options: options)
|
|
225
|
+
else
|
|
226
|
+
pgyer_message_values(result, file: file, options: options)
|
|
227
|
+
end
|
|
228
|
+
messages = templates.slice(*Message::CONTENT_FIELDS).transform_values { |body| Message.render_text(body, values) }
|
|
229
|
+
puts messages.fetch("cli_content")
|
|
230
|
+
messages
|
|
231
|
+
end
|
|
220
232
|
|
|
233
|
+
def pgyer_message_values(result, file:, options:)
|
|
221
234
|
data = result.is_a?(Hash) ? (result["data"] || result) : {}
|
|
222
235
|
key = deep_find(data, "buildKey")
|
|
223
236
|
name = deep_find(data, "buildName") || options[:app_name] || File.basename(file, File.extname(file))
|
|
@@ -240,23 +253,20 @@ module Appship
|
|
|
240
253
|
password ||= "未设置"
|
|
241
254
|
url = key ? "https://www.pgyer.com/#{key}" : "https://www.pgyer.com/"
|
|
242
255
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
puts "安装密码: #{password}"
|
|
255
|
-
puts "下载链接: #{url}"
|
|
256
|
-
puts "======================================================="
|
|
256
|
+
{
|
|
257
|
+
name: name,
|
|
258
|
+
build_type_name: build_type_name,
|
|
259
|
+
version_info: version_info,
|
|
260
|
+
configuration: configuration,
|
|
261
|
+
build_size_mb: build_size_mb,
|
|
262
|
+
build_updated: build_updated.to_s.empty? ? nil : build_updated,
|
|
263
|
+
description: description,
|
|
264
|
+
password: password,
|
|
265
|
+
url: url
|
|
266
|
+
}
|
|
257
267
|
end
|
|
258
268
|
|
|
259
|
-
def
|
|
269
|
+
def fir_message_values(result, file:, options:)
|
|
260
270
|
data = result["data"].is_a?(Hash) ? result["data"] : result
|
|
261
271
|
name = data["name"] || options[:app_name] || File.basename(file, File.extname(file))
|
|
262
272
|
type = data["type"].to_s == "android" ? "Android" : "iOS"
|
|
@@ -266,18 +276,17 @@ module Appship
|
|
|
266
276
|
build && !build.to_s.empty? ? "#{version}(#{build})" : version.to_s
|
|
267
277
|
end
|
|
268
278
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
puts "======================================================="
|
|
279
|
+
{
|
|
280
|
+
name: name,
|
|
281
|
+
build_type_name: type,
|
|
282
|
+
version_info: version_info,
|
|
283
|
+
configuration: nil,
|
|
284
|
+
build_size_mb: nil,
|
|
285
|
+
build_updated: nil,
|
|
286
|
+
description: options[:description] || "",
|
|
287
|
+
password: data["password_protected"] ? "已设置" : "未设置",
|
|
288
|
+
url: result["url"] || ""
|
|
289
|
+
}
|
|
281
290
|
end
|
|
282
291
|
|
|
283
292
|
def deep_find(value, wanted_key)
|
data/lib/appship/cache.rb
CHANGED
|
@@ -30,7 +30,11 @@ module Appship
|
|
|
30
30
|
raise ConfigurationError, missing_credentials_message(expanded, provider, empty: true)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
new(expanded)
|
|
33
|
+
cache = new(expanded)
|
|
34
|
+
return cache unless cache.entries.empty?
|
|
35
|
+
return collect_interactively!(expanded, project_dir: project_dir, provider: provider) if interactive && $stdin.tty?
|
|
36
|
+
|
|
37
|
+
raise ConfigurationError, missing_credentials_message(expanded, provider, empty: true)
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
def self.create_file!(path)
|
|
@@ -41,24 +45,29 @@ module Appship
|
|
|
41
45
|
path
|
|
42
46
|
end
|
|
43
47
|
|
|
44
|
-
def self.collect_interactively!(path, project_dir: Dir.pwd, provider: "pgyer")
|
|
45
|
-
project_name
|
|
48
|
+
def self.collect_interactively!(path, project_dir: Dir.pwd, provider: "pgyer", entries: [], project_name: nil, credential_overrides: {})
|
|
49
|
+
project_name ||= infer_project_name!(project_dir)
|
|
50
|
+
unless [".xcodeproj", ".xcworkspace"].any? { |extension| File.directory?(File.join(project_dir, "#{project_name}#{extension}")) }
|
|
51
|
+
raise ConfigurationError, "当前目录找不到 #{project_name}.xcodeproj 或 #{project_name}.xcworkspace,无法新增项目配置: #{File.expand_path(project_dir)}"
|
|
52
|
+
end
|
|
46
53
|
provider_name = provider.to_s == "fir" ? "fir.im" : "pgyer"
|
|
47
54
|
credential_key = credential_key_for(provider)
|
|
48
55
|
puts ""
|
|
49
|
-
puts "
|
|
56
|
+
puts "首次使用当前项目的 appship #{provider_name},请填写本机项目缓存。"
|
|
50
57
|
puts "缓存文件: #{path}"
|
|
51
58
|
puts ""
|
|
52
|
-
puts "✅
|
|
59
|
+
puts "✅ 已识别 project_name: #{project_name}"
|
|
53
60
|
app_name = prompt_required("app_name")
|
|
54
61
|
data = {
|
|
55
62
|
"project_name" => project_name,
|
|
56
63
|
"app_name" => app_name,
|
|
57
|
-
|
|
64
|
+
"wechat_webhook" => "",
|
|
65
|
+
"dingtalk_webhook" => "",
|
|
66
|
+
credential_key => credential_overrides[credential_key] || prompt_required(credential_key)
|
|
58
67
|
}
|
|
59
68
|
password_key = provider.to_s == "fir" ? "fir_password" : "pgyer_password"
|
|
60
|
-
data[password_key] = prompt_optional(password_key)
|
|
61
|
-
File.write(path, JSON.pretty_generate([data]) + "\n")
|
|
69
|
+
data[password_key] = credential_overrides.fetch(password_key) { prompt_optional(password_key) }
|
|
70
|
+
File.write(path, JSON.pretty_generate(entries + [data]) + "\n")
|
|
62
71
|
File.chmod(0o600, path)
|
|
63
72
|
puts ""
|
|
64
73
|
puts "✅ 本机缓存已保存: #{path}"
|
|
@@ -88,7 +97,10 @@ module Appship
|
|
|
88
97
|
|
|
89
98
|
def self.prompt_optional(name)
|
|
90
99
|
print "请输入 #{name}(可为空,直接回车表示不设置密码): "
|
|
91
|
-
$stdin.gets
|
|
100
|
+
value = $stdin.gets
|
|
101
|
+
raise ConfigurationError, "配置输入已中断,请重新执行命令继续填写" unless value
|
|
102
|
+
|
|
103
|
+
value.strip
|
|
92
104
|
end
|
|
93
105
|
|
|
94
106
|
def initialize(path = DEFAULT_PATH)
|
|
@@ -98,31 +110,55 @@ module Appship
|
|
|
98
110
|
@entries = parse(File.read(@path))
|
|
99
111
|
raise ConfigurationError, "缓存文件必须是字典数组: #{@path}" unless @entries.is_a?(Array)
|
|
100
112
|
@entries = @entries.map { |entry| normalize(entry) }
|
|
101
|
-
raise ConfigurationError, "缓存文件中没有有效项目: #{@path}" if @entries.empty?
|
|
102
113
|
rescue Psych::SyntaxError => e
|
|
103
114
|
raise ConfigurationError, "缓存文件解析失败: #{e.message}"
|
|
104
115
|
end
|
|
105
116
|
|
|
106
|
-
def resolve(project_name: nil, project_dir: Dir.pwd, provider: "pgyer", interactive: true, credential_overrides: {})
|
|
107
|
-
|
|
108
|
-
if
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
def resolve(project_name: nil, project_dir: Dir.pwd, provider: "pgyer", interactive: true, credential_overrides: {}, credentials: true)
|
|
118
|
+
candidate_names = [project_name.to_s.strip].reject(&:empty?)
|
|
119
|
+
if candidate_names.empty?
|
|
120
|
+
directory = File.expand_path(project_dir)
|
|
121
|
+
candidate_names = Dir[File.join(directory, "*.xcodeproj"), File.join(directory, "*.xcworkspace")]
|
|
122
|
+
.select { |path| File.directory?(path) }
|
|
123
|
+
.map { |path| File.basename(path, File.extname(path)) }.uniq
|
|
124
|
+
if candidate_names.empty? && @entries.length > 1
|
|
125
|
+
directory_name = File.basename(directory)
|
|
126
|
+
candidate_names = [directory_name] if @entries.any? { |entry| entry["project_name"] == directory_name }
|
|
127
|
+
end
|
|
111
128
|
end
|
|
112
129
|
|
|
113
|
-
matches = if
|
|
130
|
+
matches = if candidate_names.empty?
|
|
114
131
|
@entries
|
|
115
132
|
else
|
|
116
|
-
@entries.select { |entry| entry["project_name"]
|
|
133
|
+
@entries.select { |entry| candidate_names.include?(entry["project_name"]) }
|
|
117
134
|
end
|
|
118
135
|
if matches.length == 1
|
|
119
136
|
update_profile!(matches.first, credential_overrides) unless credential_overrides.empty?
|
|
120
|
-
ensure_provider_credential!(matches.first, provider, interactive: interactive)
|
|
137
|
+
ensure_provider_credential!(matches.first, provider, interactive: interactive) if credentials
|
|
121
138
|
return matches.first
|
|
122
139
|
end
|
|
123
140
|
if matches.empty?
|
|
141
|
+
if interactive && $stdin.tty?
|
|
142
|
+
puts "ℹ️ 当前项目没有匹配的 project_name,将引导新增项目配置。"
|
|
143
|
+
cache = self.class.collect_interactively!(
|
|
144
|
+
@path,
|
|
145
|
+
project_dir: project_dir,
|
|
146
|
+
provider: provider,
|
|
147
|
+
entries: @entries,
|
|
148
|
+
project_name: candidate_names.one? ? candidate_names.first : nil,
|
|
149
|
+
credential_overrides: credential_overrides
|
|
150
|
+
)
|
|
151
|
+
@entries = cache.entries
|
|
152
|
+
return @entries.last
|
|
153
|
+
end
|
|
154
|
+
|
|
124
155
|
available = @entries.filter_map { |entry| entry["project_name"] }.join(", ")
|
|
125
|
-
|
|
156
|
+
key = self.class.send(:credential_key_for, provider)
|
|
157
|
+
raise ConfigurationError, "当前项目没有匹配的 project_name: #{candidate_names.join(", ")}\n" \
|
|
158
|
+
"请在 #{@path} 的配置数组中增加当前项目配置,保留已有项目。\n" \
|
|
159
|
+
"project_name 应填写: #{candidate_names.join(" 或 ")}(不含 .xcodeproj/.xcworkspace 后缀)\n" \
|
|
160
|
+
"同时填写 app_name、#{key};安装密码可为空。\n" \
|
|
161
|
+
"已有项目: #{available}"
|
|
126
162
|
end
|
|
127
163
|
|
|
128
164
|
names = matches.filter_map { |entry| entry["project_name"] }.join(", ")
|