fastlane-plugin-fastci 0.0.40 → 0.0.41
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 +153 -4
- data/lib/fastlane/plugin/fastci/actions/package_action.rb +73 -117
- data/lib/fastlane/plugin/fastci/actions/upload_store_action.rb +31 -6
- data/lib/fastlane/plugin/fastci/helper/dingding_helper.rb +57 -22
- data/lib/fastlane/plugin/fastci/helper/dingding_image_helper.rb +70 -0
- data/lib/fastlane/plugin/fastci/helper/environment.rb +13 -0
- data/lib/fastlane/plugin/fastci/helper/package_notification_helper.rb +184 -0
- data/lib/fastlane/plugin/fastci/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 834603cf8a0a78c4fec22ee9f17d84a5f491d25770b2972c3e85b0b5ffcfcc27
|
|
4
|
+
data.tar.gz: a9ded28f219cd9215058581204ac445601479c0978b4e393f99ed0835f6fe461
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad0be57cc0708ecdf1b920bb1c3ebff28b94a25a10adde0e7be43b4ba967b9bf95a95d3a3946b6c06ba24fec1d05cd84e91a38e8a313d80868d5c4589a0a084e
|
|
7
|
+
data.tar.gz: b17392ce988cc5279ef375a8a474288cb51183d7dd0a4b8ec4191351162cb0bc6204e1a782af7f53b9505aab27832cd4666c33cf39208923869245dcc5c9ed06
|
data/README.md
CHANGED
|
@@ -71,7 +71,7 @@ package(
|
|
|
71
71
|
is_detect_duplicity_code: false, # 是否检测重复代码
|
|
72
72
|
is_detect_unused_code: false, # 是否检测未使用代码
|
|
73
73
|
is_detect_unused_image: false, # 是否检测未使用图片
|
|
74
|
-
changelog: options[:changelog], # fir / Zealot
|
|
74
|
+
changelog: options[:changelog], # fir / Zealot 更新日志,也是各平台钉钉通知的更新说明
|
|
75
75
|
app_store_resources_enabled: options[:app_store_resources_enabled], # 总开关;未传时读取 APP_STORE_RESOURCES_ENABLED,默认 false
|
|
76
76
|
app_store_resources_config_path: options[:app_store_resources_config_path], # 未传时读取 APP_STORE_RESOURCES_CONFIG_PATH
|
|
77
77
|
release_notes: options[:release_notes], # JSON 字符串,例如 '{"zh-Hans":"修复问题","en-US":"Bug fixes"}';仅资源功能开启且上传 metadata 时生效
|
|
@@ -401,6 +401,155 @@ sync_zealot_devices(api_key_path: "./app_store_connect_api_key.json")
|
|
|
401
401
|
|
|
402
402
|
依赖兼容:Zealot 的 `debug_file` 依赖 `ruby-macho < 3`,不能与 CocoaPods 1.17(依赖 4.x)放在同一 bundle。此类工程应将 CocoaPods 放入独立 Gemfile;使用 Faraday 1 的 Fastlane 工程还应将 `faraday-net_http` 限制为 `~> 1.0`。
|
|
403
403
|
|
|
404
|
+
### 8. 钉钉通知
|
|
405
|
+
|
|
406
|
+
`package` 使用统一的 Markdown 模板发送分发结果、失败及检查结果。配置 `DINGDING_TOKEN` 后默认开启,传入 `is_notice_dingding: false` 可关闭本次流程的全部通知。机器人配置参考[钉钉自定义机器人文档](https://open.dingtalk.com/document/robots/custom-robot-access),关键词可设为 `iOS`。
|
|
407
|
+
|
|
408
|
+
```dotenv
|
|
409
|
+
DINGDING_TOKEN=
|
|
410
|
+
DINGDING_INCLUDE_INSTALL_PASSWORD=true
|
|
411
|
+
DINGDING_TESTFLIGHT_URL=
|
|
412
|
+
DINGDING_APP_STORE_URL=
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
| 配置 | 默认值 | 用途 |
|
|
416
|
+
| --- | --- | --- |
|
|
417
|
+
| `DINGDING_TOKEN` | 未设置 | 群机器人的 `access_token`;未配置时不发送 |
|
|
418
|
+
| `is_notice_dingding` | `true` | `package` 参数;`false` 优先于 Token 配置 |
|
|
419
|
+
| `DINGDING_INCLUDE_INSTALL_PASSWORD` | `true` | 是否展示蒲公英 / fir 的安装密码;`false` 时隐藏 |
|
|
420
|
+
| `DINGDING_TESTFLIGHT_URL` | 未设置 | TestFlight 公开测试邀请链接,配置后展示 |
|
|
421
|
+
| `DINGDING_APP_STORE_URL` | 未设置 | App Store 线上应用页面,配置后展示 |
|
|
422
|
+
|
|
423
|
+
#### 统一内容与发送时机
|
|
424
|
+
|
|
425
|
+
标题为 `应用 Scheme iOS · 平台 · 状态`。各平台均包含应用、Bundle ID、分发平台、状态、实际版本、Build、打包方式、构建配置、累计耗时和通知时间;时间包含构建机器的时区偏移。`changelog` 有值时追加更新说明,最多展示 1,000 个字符。缺失的可选字段与链接不展示。
|
|
426
|
+
|
|
427
|
+
更新说明保留输入的逐行内容,LF / CRLF 换行会转换为 Markdown 强制换行,避免多条 Git 提交显示成一段;该格式转换仅用于钉钉展示,不修改上传平台收到的日志原文。
|
|
428
|
+
|
|
429
|
+
二维码由构建机下载后,以 `data:image/...;base64,...` 内嵌到同一条 Markdown 通知,保留「打开二维码」链接。接受 PNG / JPEG / GIF,单图上限 8 KB,连接超时 5 秒、下载总超时 15 秒;下载失败、格式不符或超过上限时只展示链接,不中断打包。Base64 展示采用当前使用环境已验证的方式;[钉钉官方消息类型文档](https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type)未明确承诺 Data URL 的跨客户端支持。
|
|
430
|
+
|
|
431
|
+
- IPA 构建并复制完成后仅记录终端日志,不发送钉钉成功通知。
|
|
432
|
+
- 蒲公英、fir、Zealot、TestFlight 和 App Store 在各自上传结束后发送一次平台结果;同时上传多个平台时,每个平台各一条。
|
|
433
|
+
- 商店包缺少上传配置时发送「未上传」,说明 IPA 已生成;未配置测试分发平台且未执行检查时,构建成功不发送通知。
|
|
434
|
+
- 构建、分发、符号上传或检查抛出异常时,发送当前平台和失败阶段,随后保留原有异常处理。原始异常详情仍在构建日志中。
|
|
435
|
+
- Zealot 关闭 `fail_on_error` 后未返回发布结果时,发送「上传未完成」,按原配置继续流程。
|
|
436
|
+
- 只有实际执行了代码检查,才发送检查项目清单;这表示检查执行完成,问题详情查看报告。App Store / TestFlight 跳过检查时不发送检查完成通知。
|
|
437
|
+
- 钉钉发送超时、HTTP 失败或机器人拒绝消息时,只记录提示,不改变打包结果。不再发送「测试一下」。
|
|
438
|
+
|
|
439
|
+
发送使用系统 `/usr/bin/curl`,保留 HTTPS 证书校验,连接超时 5 秒、整个请求超时 15 秒;Token 与正文通过标准输入传入,不出现在命令参数中。终端会记录发送开始及成功,失败时区分 curl 退出码、HTTP 状态码和钉钉 `errcode`,不打印凭据或原始响应。
|
|
440
|
+
|
|
441
|
+
通知由 `package` 统一编排;直接调用 `upload_pgy`、`upload_fir`、`upload_zealot`、`upload_store` 或 `app_store_resources` 不额外发送这套通知。
|
|
442
|
+
|
|
443
|
+
#### 各分发平台内容
|
|
444
|
+
|
|
445
|
+
下面五份示例使用同一套公共字段;安装密码、链接、二维码只在配置或上传结果提供时出现。示例中的更新说明来自 `package(changelog: "修复问题,优化体验")`,不会自动读取 metadata 目录中的文案。
|
|
446
|
+
|
|
447
|
+
**蒲公英**:安装页面、安装二维码、安装密码。地址使用上传接口返回的实际地址。
|
|
448
|
+
|
|
449
|
+
```text
|
|
450
|
+
📦 ExampleApp iOS · 蒲公英 · 上传完成
|
|
451
|
+
应用:ExampleApp
|
|
452
|
+
Bundle ID:com.example.app
|
|
453
|
+
分发平台:蒲公英
|
|
454
|
+
状态:上传完成
|
|
455
|
+
版本:1.2.3
|
|
456
|
+
Build:42
|
|
457
|
+
打包方式:ad-hoc
|
|
458
|
+
构建配置:Release
|
|
459
|
+
安装密码:example-password
|
|
460
|
+
累计耗时:180 秒
|
|
461
|
+
通知时间:2026-11-03 12:00:00 +08:00
|
|
462
|
+
[安装页面]
|
|
463
|
+
[安装二维码]
|
|
464
|
+
更新说明:修复问题,优化体验
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**fir**:安装页面、安装密码。当前上传接口不提供二维码,因此不生成二维码链接。
|
|
468
|
+
|
|
469
|
+
```text
|
|
470
|
+
📦 ExampleApp iOS · fir · 上传完成
|
|
471
|
+
应用:ExampleApp
|
|
472
|
+
Bundle ID:com.example.app
|
|
473
|
+
分发平台:fir
|
|
474
|
+
状态:上传完成
|
|
475
|
+
版本:1.2.3
|
|
476
|
+
Build:42
|
|
477
|
+
打包方式:ad-hoc
|
|
478
|
+
构建配置:Release
|
|
479
|
+
安装密码:example-password
|
|
480
|
+
累计耗时:190 秒
|
|
481
|
+
通知时间:2026-11-03 12:00:10 +08:00
|
|
482
|
+
[安装页面]
|
|
483
|
+
更新说明:修复问题,优化体验
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Zealot**:Release ID、版本详情、安装页面、安装二维码,以及 dSYM 是否上传。开启 dSYM 上传后,等待该步骤结束再发送此平台的结果。
|
|
487
|
+
|
|
488
|
+
```text
|
|
489
|
+
📦 ExampleApp iOS · Zealot · 上传完成
|
|
490
|
+
应用:ExampleApp
|
|
491
|
+
Bundle ID:com.example.app
|
|
492
|
+
分发平台:Zealot
|
|
493
|
+
状态:上传完成
|
|
494
|
+
版本:1.2.3
|
|
495
|
+
Build:42
|
|
496
|
+
打包方式:ad-hoc
|
|
497
|
+
构建配置:Release
|
|
498
|
+
Release ID:42
|
|
499
|
+
dSYM:已上传
|
|
500
|
+
累计耗时:200 秒
|
|
501
|
+
通知时间:2026-11-03 12:00:20 +08:00
|
|
502
|
+
[版本详情] [安装页面]
|
|
503
|
+
[安装二维码]
|
|
504
|
+
更新说明:修复问题,优化体验
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**TestFlight**:App Store Connect 入口、可选测试邀请;明确区分 IPA 上传和 Apple 后续处理。邀请链接不会因为本次上传而保证新构建已可测试。
|
|
508
|
+
|
|
509
|
+
```text
|
|
510
|
+
📦 ExampleApp iOS · TestFlight · 上传完成
|
|
511
|
+
应用:ExampleApp
|
|
512
|
+
Bundle ID:com.example.app
|
|
513
|
+
分发平台:TestFlight
|
|
514
|
+
状态:上传完成
|
|
515
|
+
版本:1.2.3
|
|
516
|
+
Build:42
|
|
517
|
+
打包方式:testFlight
|
|
518
|
+
构建配置:Release
|
|
519
|
+
说明:IPA 已上传,尚未等待 Apple 处理完成;测试可用性请在 App Store Connect 确认。
|
|
520
|
+
累计耗时:240 秒
|
|
521
|
+
通知时间:2026-11-03 12:01:00 +08:00
|
|
522
|
+
[App Store Connect] [TestFlight 测试邀请]
|
|
523
|
+
更新说明:修复问题,优化体验
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**App Store**:App Store Connect 入口、可选线上页面,以及本次元数据、截图、构建选择、提交审核的执行结果。资源总开关关闭时显示「商店资源:未开启,仅上传 IPA」;开启时如下例。
|
|
527
|
+
|
|
528
|
+
```text
|
|
529
|
+
📦 ExampleApp iOS · App Store · 上传完成
|
|
530
|
+
应用:ExampleApp
|
|
531
|
+
Bundle ID:com.example.app
|
|
532
|
+
分发平台:App Store
|
|
533
|
+
状态:上传完成
|
|
534
|
+
版本:1.2.3
|
|
535
|
+
Build:42
|
|
536
|
+
打包方式:app-store
|
|
537
|
+
构建配置:Release
|
|
538
|
+
说明:上传完成不代表审核通过或已上线;线上页面可能仍为旧版本。
|
|
539
|
+
元数据:已上传
|
|
540
|
+
截图:未上传
|
|
541
|
+
构建选择:已选择本次 Build
|
|
542
|
+
提交审核:已提交
|
|
543
|
+
累计耗时:360 秒
|
|
544
|
+
通知时间:2026-11-03 12:03:00 +08:00
|
|
545
|
+
[App Store Connect] [App Store 线上页面]
|
|
546
|
+
更新说明:修复问题,优化体验
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
商店资源流程失败时不会发送上述成功消息,而是发送「执行失败」,注明失败阶段和 IPA 是否已经上传;通知本身不会开启商店资源功能。
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
404
553
|
## 发布到 RubyGems
|
|
405
554
|
|
|
406
555
|
工作流位于 [`.github/workflows/release.yml`](.github/workflows/release.yml)。使用 `1.0.0` 这样的纯版本标签,无需 `v` 前缀。推送数字开头的标签后,自动校验标签与 `lib/fastlane/plugin/fastci/version.rb` 中的 `VERSION` 完全一致,检查 Ruby 语法、构建 gem 并发布到 RubyGems。
|
|
@@ -418,12 +567,12 @@ sync_zealot_devices(api_key_path: "./app_store_connect_api_key.json")
|
|
|
418
567
|
|
|
419
568
|
工作流使用官方 OIDC 短期凭据,不需要配置 `RUBYGEMS_API_KEY` 或创建 GitHub Environment。工作流文件在当前仓库内,Workflow Repository 两项留空。若以后设置 Environment,RubyGems 配置与工作流的 `environment` 必须保持一致。
|
|
420
569
|
|
|
421
|
-
每次发布先修改 `VERSION`,执行 `bundle install` 同步 `Gemfile.lock`,并提交本次拟发布的代码和工作流。然后推送对应标签,例如版本更新为 `0.0.
|
|
570
|
+
每次发布先修改 `VERSION`,执行 `bundle install` 同步 `Gemfile.lock`,并提交本次拟发布的代码和工作流。然后推送对应标签,例如版本更新为 `0.0.41` 后:
|
|
422
571
|
|
|
423
572
|
```bash
|
|
424
573
|
git push origin main
|
|
425
|
-
git tag -a 0.0.
|
|
426
|
-
git push origin 0.0.
|
|
574
|
+
git tag -a 0.0.41 -m "Release 0.0.41"
|
|
575
|
+
git push origin 0.0.41
|
|
427
576
|
```
|
|
428
577
|
|
|
429
578
|
到 GitHub **Actions → Release to RubyGems** 查看结果。已发布版本不能覆盖;失败重跑前先确认该版本是否已经发布。只有打入标签提交的文件会被发布,本地未提交改动不会包含在内。
|
|
@@ -2,6 +2,7 @@ require 'gym'
|
|
|
2
2
|
require 'fastlane_core'
|
|
3
3
|
require 'fastlane/action'
|
|
4
4
|
require 'fastlane/plugin/fastci/helper/app_store_resources_helper'
|
|
5
|
+
require 'fastlane/plugin/fastci/helper/package_notification_helper'
|
|
5
6
|
include Fastlane::Helper
|
|
6
7
|
|
|
7
8
|
module Fastlane
|
|
@@ -9,27 +10,14 @@ module Fastlane
|
|
|
9
10
|
# 打包
|
|
10
11
|
class PackageAction < Action
|
|
11
12
|
def self.run(params)
|
|
13
|
+
notice = PackageNotificationHelper.new(params)
|
|
12
14
|
# 入参配置
|
|
13
15
|
configuration = params[:configuration] || 'Debug'
|
|
14
16
|
export_method = params[:export_method] || 'development'
|
|
15
17
|
build = params[:build] || nil
|
|
16
18
|
version = params[:version] || nil
|
|
17
|
-
is_analyze_swiftlint = params[:is_analyze_swiftlint] || false
|
|
18
|
-
is_detect_duplicity_code = params[:is_detect_duplicity_code] || false
|
|
19
|
-
is_detect_unused_code = params[:is_detect_unused_code] || false
|
|
20
|
-
is_detect_unused_image = params[:is_detect_unused_image] || false
|
|
21
19
|
store_options = store_upload_options(params, export_method)
|
|
22
20
|
configuration = 'Release' if %w[app-store testFlight].include?(export_method)
|
|
23
|
-
is_notice_dingding = params[:is_notice_dingding].nil? ? true : params[:is_notice_dingding]
|
|
24
|
-
# 统一发送钉钉消息匿名函数
|
|
25
|
-
send_dingding_notice = lambda do |text|
|
|
26
|
-
if is_notice_dingding
|
|
27
|
-
DingdingHelper.sendMarkdown(text)
|
|
28
|
-
else
|
|
29
|
-
UI.message('*************| 跳过钉钉消息通知(is_notice_dingding=false)|*************')
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
send_dingding_notice.call('测试一下')
|
|
33
21
|
# 清理上一次的打包缓存
|
|
34
22
|
FileUtils.rm_rf(Dir.glob("#{Constants.BUILD_LOG_DIR}/*"))
|
|
35
23
|
FileUtils.rm_rf(Dir.glob("#{Constants.IPA_OUTPUT_DIR}/*"))
|
|
@@ -53,7 +41,6 @@ module Fastlane
|
|
|
53
41
|
other_action.update_build_number(
|
|
54
42
|
build: build
|
|
55
43
|
)
|
|
56
|
-
time = Time.new.strftime('%Y%m%d%H%M')
|
|
57
44
|
|
|
58
45
|
# 获取版本号
|
|
59
46
|
version_options = { target: scheme }
|
|
@@ -66,6 +53,7 @@ module Fastlane
|
|
|
66
53
|
build = other_action.get_build_number(build_options)
|
|
67
54
|
# 生成ipa包的名字格式
|
|
68
55
|
ipaName = "#{scheme}_#{export_method}_#{version}_#{build}.ipa"
|
|
56
|
+
notice.build_info(version: version, build: build)
|
|
69
57
|
|
|
70
58
|
# 获取 Extension 的 Bundle ID(可能有多个,用逗号分隔)
|
|
71
59
|
extension_bundle_ids = Environment.extension_bundle_ids
|
|
@@ -120,11 +108,13 @@ module Fastlane
|
|
|
120
108
|
gym_options[:export_options][:provisioningProfiles] = provisioningProfiles_map
|
|
121
109
|
end
|
|
122
110
|
|
|
111
|
+
notice.stage(:local, '构建 IPA')
|
|
123
112
|
other_action.gym(gym_options)
|
|
124
113
|
|
|
125
114
|
UI.message('*************| 打包完成 |*************')
|
|
126
115
|
|
|
127
116
|
UI.message('*************| 复制打包产物 |*************')
|
|
117
|
+
notice.stage(:local, '复制构建产物')
|
|
128
118
|
# 定义桌面路径
|
|
129
119
|
desktop_path = File.expand_path('~/Desktop')
|
|
130
120
|
output_path = File.join(desktop_path, "BuildOutput_#{scheme}")
|
|
@@ -141,126 +131,92 @@ module Fastlane
|
|
|
141
131
|
# system("git reset --hard")
|
|
142
132
|
|
|
143
133
|
ipa_path = "#{Constants.IPA_OUTPUT_DIR}/#{ipaName}"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
notiText = "🚀🚀🚀🚀🚀🚀\n\n#{scheme}-iOS-打包完成\n\n#{version}_#{build}_#{export_method}\n\n🚀🚀🚀🚀🚀🚀"
|
|
147
|
-
send_dingding_notice.call(notiText)
|
|
148
|
-
|
|
149
|
-
if upload_store_if_configured(store_options, version: version, build: build)
|
|
150
|
-
notiText = "🚀🚀🚀🚀🚀🚀\n\n#{scheme}-iOS-上传完成\n\n#{version}_#{build}_#{export_method}\n\n🚀🚀🚀🚀🚀🚀"
|
|
151
|
-
send_dingding_notice.call(notiText)
|
|
152
|
-
end
|
|
153
|
-
else
|
|
154
|
-
# 钉钉通知
|
|
155
|
-
notiText = "🚀🚀🚀🚀🚀🚀\n\n#{scheme}-iOS-打包完成\n\n#{version}_#{build}_#{export_method}\n\n🚀🚀🚀🚀🚀🚀"
|
|
156
|
-
|
|
157
|
-
# 上传蒲公英
|
|
158
|
-
if CommonHelper.is_validate_string(Environment.pgy_api_key)
|
|
159
|
-
pgy_upload_info = other_action.upload_pgy
|
|
160
|
-
qrCode = pgy_upload_info['buildQRCodeURL']
|
|
161
|
-
|
|
162
|
-
if CommonHelper.is_validate_string(qrCode)
|
|
163
|
-
notiText << "\n\n⬇️⬇️⬇️ 扫码安装 ⬇️⬇️⬇️\n\n\n密码: #{Environment.pgy_password}\n"
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# 上传 fir
|
|
168
|
-
if CommonHelper.is_validate_string(Environment.fir_api_token)
|
|
169
|
-
fir_upload_info = other_action.upload_fir(
|
|
170
|
-
changelog: params[:changelog]
|
|
171
|
-
)
|
|
172
|
-
download_url = fir_upload_info[:download_url]
|
|
173
|
-
|
|
174
|
-
if CommonHelper.is_validate_string(download_url)
|
|
175
|
-
notiText << "\n\n⬇️⬇️⬇️ 点击链接安装 ⬇️⬇️⬇️\n\n\n密码: #{Environment.fir_password}\n[_点击下载_](#{download_url})"
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# 上传 Zealot
|
|
180
|
-
if CommonHelper.is_validate_string(Environment.zealot_token)
|
|
181
|
-
zealot_upload_info = other_action.upload_zealot(
|
|
182
|
-
file: ipa_path,
|
|
183
|
-
changelog: params[:changelog]
|
|
184
|
-
)
|
|
185
|
-
# dSYM 仅在 IPA 上传成功且显式开启时上传。
|
|
186
|
-
if zealot_upload_info && params[:is_upload_zealot_dsym]
|
|
187
|
-
other_action.upload_zealot_dsym
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
send_dingding_notice.call(notiText)
|
|
192
|
-
end
|
|
134
|
+
# 构建完成仅记录终端日志,钉钉成功通知由各分发平台上传结果触发。
|
|
135
|
+
distribute_package(params, notice, store_options, ipa_path: ipa_path, version: version, build: build)
|
|
193
136
|
|
|
194
137
|
# Sentry 上传 dSYM
|
|
195
138
|
if CommonHelper.is_validate_string(Environment.sentry_auth_token)
|
|
139
|
+
notice.stage(:sentry, '上传 dSYM')
|
|
196
140
|
other_action.sentry_upload_dsym
|
|
197
141
|
else
|
|
198
142
|
UI.message('*************| 未配置 Sentry 跳过 dSYM 上传 |*************')
|
|
199
143
|
end
|
|
200
144
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
other_action.analyze_swiftlint(
|
|
204
|
-
is_from_package: true,
|
|
205
|
-
configuration: configuration
|
|
206
|
-
)
|
|
207
|
-
# 结果复制到桌面
|
|
208
|
-
FileUtils.cp(SWIFTLINT_ANALYZE_HTML_FILE, target_path)
|
|
209
|
-
UI.message('*************| 代码分析完成 |*************')
|
|
210
|
-
else
|
|
211
|
-
UI.message('*************| 跳过代码分析 |*************')
|
|
212
|
-
end
|
|
145
|
+
run_package_checks(params, notice, gym_method: gym_method, scheme: scheme,
|
|
146
|
+
configuration: configuration, target_path: target_path)
|
|
213
147
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
# 结果复制到桌面
|
|
220
|
-
FileUtils.cp(DUPLICITY_CODE_HTML_FILE, target_path)
|
|
221
|
-
UI.message('*************| 重复代码检查完成 |*************')
|
|
222
|
-
else
|
|
223
|
-
UI.message('*************| 跳过重复代码检查 |*************')
|
|
224
|
-
end
|
|
148
|
+
UI.message('*************| 脚本完成 |*************')
|
|
149
|
+
rescue StandardError
|
|
150
|
+
notice&.failure(store_result: Actions.lane_context[SharedValues::FASTCI_STORE_UPLOAD_RESULT] || {})
|
|
151
|
+
raise
|
|
152
|
+
end
|
|
225
153
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
154
|
+
# 各平台分别发送结果;后一个平台失败时,已经成功的平台仍有明确通知。
|
|
155
|
+
def self.distribute_package(params, notice, store_options, ipa_path:, version:, build:)
|
|
156
|
+
if %w[app-store testFlight].include?(params[:export_method])
|
|
157
|
+
platform = params[:export_method] == 'testFlight' ? :testflight : :app_store
|
|
158
|
+
notice.stage(platform, '准备商店上传')
|
|
159
|
+
Actions.lane_context.delete(SharedValues::FASTCI_STORE_UPLOAD_RESULT)
|
|
160
|
+
if upload_store_if_configured(store_options, version: version, build: build)
|
|
161
|
+
result = Actions.lane_context[SharedValues::FASTCI_STORE_UPLOAD_RESULT] || {}
|
|
162
|
+
notice.distribution_completed(platform, { uploaded: true }, store_result: result)
|
|
163
|
+
else
|
|
164
|
+
notice.store_skipped(platform)
|
|
165
|
+
end
|
|
236
166
|
else
|
|
237
|
-
|
|
167
|
+
distribute_test_package(params, notice, ipa_path)
|
|
238
168
|
end
|
|
169
|
+
end
|
|
239
170
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
UI.message('*************| 无用图片检查完成 |*************')
|
|
246
|
-
else
|
|
247
|
-
UI.message('*************| 跳过未使用图片检查 |*************')
|
|
171
|
+
# 保持原有上传顺序与开关;安装链接的整理交给通知 Helper。
|
|
172
|
+
def self.distribute_test_package(params, notice, ipa_path)
|
|
173
|
+
if CommonHelper.is_validate_string(Environment.pgy_api_key)
|
|
174
|
+
notice.stage(:pgy, '上传 IPA')
|
|
175
|
+
notice.distribution_completed(:pgy, other_action.upload_pgy)
|
|
248
176
|
end
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
177
|
+
if CommonHelper.is_validate_string(Environment.fir_api_token)
|
|
178
|
+
notice.stage(:fir, '上传 IPA')
|
|
179
|
+
notice.distribution_completed(:fir, other_action.upload_fir(changelog: params[:changelog]))
|
|
180
|
+
end
|
|
181
|
+
return unless CommonHelper.is_validate_string(Environment.zealot_token)
|
|
182
|
+
|
|
183
|
+
notice.stage(:zealot, '上传 IPA')
|
|
184
|
+
result = other_action.upload_zealot(file: ipa_path, changelog: params[:changelog])
|
|
185
|
+
if result && params[:is_upload_zealot_dsym]
|
|
186
|
+
notice.stage(:zealot, '上传 dSYM(IPA 已上传)')
|
|
187
|
+
other_action.upload_zealot_dsym
|
|
188
|
+
result = result.merge(dsym_status: Actions.lane_context[SharedValues::ZEAALOT_ERROR_MESSAGE] ? '上传未完成' : '已上传')
|
|
259
189
|
end
|
|
190
|
+
notice.distribution_completed(:zealot, result)
|
|
191
|
+
end
|
|
260
192
|
|
|
261
|
-
|
|
193
|
+
# 仅统计实际调用的检查,商店包跳过检查时不发送“检查完成”。
|
|
194
|
+
def self.run_package_checks(params, notice, gym_method:, scheme:, configuration:, target_path:)
|
|
195
|
+
return if gym_method == 'app-store'
|
|
196
|
+
|
|
197
|
+
checks = [
|
|
198
|
+
[:is_analyze_swiftlint, 'SwiftLint', :analyze_swiftlint,
|
|
199
|
+
{ is_from_package: true, configuration: configuration }, Constants.SWIFTLINT_ANALYZE_HTML_FILE],
|
|
200
|
+
[:is_detect_duplicity_code, '重复代码', :detect_duplicity_code,
|
|
201
|
+
{ is_all: true }, Constants.DUPLICITY_CODE_HTML_FILE],
|
|
202
|
+
[:is_detect_unused_code, '无用代码', :detect_unused_code,
|
|
203
|
+
{ scheme: scheme, is_from_package: true, configuration: configuration }, Constants.UNUSED_CODE_HTML_FILE],
|
|
204
|
+
[:is_detect_unused_image, '无用图片', :detect_unused_image, {}, Constants.UNUSED_IMAGE_HTML_FILE]
|
|
205
|
+
]
|
|
206
|
+
completed = []
|
|
207
|
+
checks.each do |key, name, action, options, report|
|
|
208
|
+
next unless params[key]
|
|
209
|
+
|
|
210
|
+
notice.stage(:checks, name)
|
|
211
|
+
options.empty? ? other_action.public_send(action) : other_action.public_send(action, **options)
|
|
212
|
+
FileUtils.cp(report, target_path)
|
|
213
|
+
completed << name
|
|
214
|
+
end
|
|
215
|
+
notice.checks_completed(completed)
|
|
262
216
|
end
|
|
263
217
|
|
|
218
|
+
private_class_method :distribute_package, :distribute_test_package, :run_package_checks
|
|
219
|
+
|
|
264
220
|
# 构建前准备商店上传参数,开启资源功能却漏配路径时立即报错。
|
|
265
221
|
def self.store_upload_options(params, export_method)
|
|
266
222
|
config_path = AppStoreResourcesHelper.upload_config_path(params, app_store: export_method == 'app-store')
|
|
@@ -5,15 +5,23 @@ include Fastlane::Helper
|
|
|
5
5
|
|
|
6
6
|
module Fastlane
|
|
7
7
|
module Actions
|
|
8
|
+
module SharedValues
|
|
9
|
+
FASTCI_STORE_UPLOAD_RESULT = :FASTCI_STORE_UPLOAD_RESULT
|
|
10
|
+
end
|
|
11
|
+
|
|
8
12
|
# 上传 AppStore
|
|
9
13
|
class UploadStoreAction < Action
|
|
10
14
|
def self.run(params)
|
|
15
|
+
# 每次上传重新记录阶段;失败时 package 可区分 IPA 上传与后续资源发布。
|
|
16
|
+
result = { stage: '准备商店上传', ipa_uploaded: false, resources_enabled: false }
|
|
17
|
+
Actions.lane_context[SharedValues::FASTCI_STORE_UPLOAD_RESULT] = result
|
|
11
18
|
resource_params, api_key_options = prepare_upload(params)
|
|
19
|
+
result[:resources_enabled] = !resource_params.nil?
|
|
12
20
|
|
|
13
21
|
if params[:isTestFlight]
|
|
14
|
-
upload_testflight(api_key_options)
|
|
22
|
+
upload_testflight(api_key_options, result)
|
|
15
23
|
else
|
|
16
|
-
upload_app_store(resource_params, api_key_options)
|
|
24
|
+
upload_app_store(resource_params, api_key_options, result)
|
|
17
25
|
end
|
|
18
26
|
end
|
|
19
27
|
|
|
@@ -51,23 +59,40 @@ module Fastlane
|
|
|
51
59
|
end
|
|
52
60
|
|
|
53
61
|
# TestFlight 沿用现有上传方式,不执行 App Store 资源流程。
|
|
54
|
-
def self.upload_testflight(api_key_options)
|
|
62
|
+
def self.upload_testflight(api_key_options, result)
|
|
55
63
|
UI.message('*************| 开始上传 TestFlight |*************')
|
|
56
|
-
|
|
64
|
+
result[:stage] = '上传 TestFlight IPA'
|
|
65
|
+
upload_result = other_action.upload_to_testflight({ skip_waiting_for_build_processing: true }.merge(api_key_options))
|
|
66
|
+
result[:ipa_uploaded] = true
|
|
67
|
+
upload_result
|
|
57
68
|
end
|
|
58
69
|
|
|
59
70
|
# 先上传 IPA,再处理已校验的资源;返回 IPA 上传结果以兼容已有 lane。
|
|
60
|
-
def self.upload_app_store(resource_params, api_key_options)
|
|
71
|
+
def self.upload_app_store(resource_params, api_key_options, result)
|
|
61
72
|
UI.message('*************| 开始上传 AppStore |*************')
|
|
73
|
+
result[:stage] = '上传 App Store IPA'
|
|
62
74
|
upload_options = app_store_upload_options(resource_params).merge(api_key_options)
|
|
63
75
|
upload_result = other_action.upload_to_app_store(upload_options)
|
|
76
|
+
result[:ipa_uploaded] = true
|
|
64
77
|
if resource_params
|
|
78
|
+
result[:stage] = '发布商店资源 / 选择构建 / 提交审核'
|
|
65
79
|
# 新版本判断已在 IPA 上传前完成,之后直接使用确认过的资源上传开关。
|
|
66
80
|
other_action.app_store_resources(resource_params.merge(upload_metadata_on_new_version: false))
|
|
81
|
+
record_resource_result(result, resource_params)
|
|
67
82
|
end
|
|
68
83
|
upload_result
|
|
69
84
|
end
|
|
70
85
|
|
|
86
|
+
# 仅在整个资源 Action 成功返回后记录完成项,保持原 Action 的返回值不变。
|
|
87
|
+
def self.record_resource_result(result, params)
|
|
88
|
+
result.merge!(
|
|
89
|
+
metadata_uploaded: params[:metadata_changed],
|
|
90
|
+
screenshots_uploaded: params[:screenshots_changed],
|
|
91
|
+
build_selected: params[:select_build] || params[:submit_for_review],
|
|
92
|
+
review_submitted: params[:submit_for_review]
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
71
96
|
# IPA 阶段始终关闭资源上传,避免默认目录或 Deliverfile 绕过资源总开关。
|
|
72
97
|
def self.app_store_upload_options(resource_params)
|
|
73
98
|
options = {
|
|
@@ -90,7 +115,7 @@ module Fastlane
|
|
|
90
115
|
end
|
|
91
116
|
|
|
92
117
|
private_class_method :prepare_upload, :load_store_resources, :parse_release_notes,
|
|
93
|
-
:upload_testflight, :upload_app_store, :app_store_upload_options
|
|
118
|
+
:upload_testflight, :upload_app_store, :app_store_upload_options, :record_resource_result
|
|
94
119
|
|
|
95
120
|
def self.description
|
|
96
121
|
"上传 AppStore"
|
|
@@ -1,35 +1,70 @@
|
|
|
1
1
|
require 'fastlane_core/ui/ui'
|
|
2
|
-
require '
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'uri'
|
|
3
5
|
|
|
4
6
|
module Fastlane
|
|
5
7
|
module Helper
|
|
6
8
|
class DingdingHelper
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
UI.message(
|
|
13
|
-
return
|
|
10
|
+
# 保留原调用入口;JSON 序列化避免更新说明中的引号、换行破坏消息。
|
|
11
|
+
def self.sendMarkdown(text, title: nil)
|
|
12
|
+
token = Environment.dingdingToken.to_s.strip
|
|
13
|
+
if token.empty?
|
|
14
|
+
UI.message('跳过钉钉通知:未配置 DINGDING_TOKEN')
|
|
15
|
+
return false
|
|
14
16
|
end
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
curl 'https://oapi.dingtalk.com/robot/send?access_token=#{Environment.dingdingToken}' \
|
|
20
|
-
-H 'Content-Type:application/json' \
|
|
21
|
-
-d '{
|
|
22
|
-
"msgtype":"markdown",
|
|
23
|
-
"markdown":{
|
|
24
|
-
"title":"#{Environment.scheme} 打包通知",
|
|
25
|
-
"text":"#{text}"
|
|
26
|
-
}
|
|
27
|
-
}'
|
|
17
|
+
|
|
18
|
+
payload = {
|
|
19
|
+
msgtype: 'markdown',
|
|
20
|
+
markdown: { title: title || "#{Environment.scheme} 打包通知", text: text }
|
|
28
21
|
}
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
UI.message('开始发送钉钉通知')
|
|
23
|
+
response = post_message(token, payload)
|
|
24
|
+
return false unless response
|
|
25
|
+
|
|
26
|
+
unless response[:code].between?(200, 299)
|
|
27
|
+
UI.important("钉钉通知发送失败(HTTP #{response[:code]}),继续原流程")
|
|
28
|
+
return false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
result = JSON.parse(response[:body])
|
|
32
|
+
if result.is_a?(Hash) && result['errcode'] == 0
|
|
33
|
+
UI.success('钉钉通知发送成功')
|
|
34
|
+
return true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# 不输出响应原文或异常消息,避免机器人 Token 随请求地址进入日志。
|
|
38
|
+
error_code = result.is_a?(Hash) && result['errcode'].is_a?(Integer) ? result['errcode'] : '未知'
|
|
39
|
+
UI.important("钉钉拒绝消息(errcode=#{error_code}),请检查机器人配置;继续原流程")
|
|
40
|
+
false
|
|
41
|
+
rescue StandardError => error
|
|
42
|
+
UI.important("钉钉通知发送失败(#{error.class}),请检查网络及机器人配置;继续原流程")
|
|
43
|
+
false
|
|
31
44
|
end
|
|
32
45
|
|
|
46
|
+
# 使用系统 curl 的证书链校验,避开 Ruby OpenSSL 在本机的 CRL 校验错误。
|
|
47
|
+
# URL 和 JSON 通过 stdin 配置传递,不进入进程参数或 shell;保持 TLS 校验与超时。
|
|
48
|
+
def self.post_message(token, payload)
|
|
49
|
+
uri = URI('https://oapi.dingtalk.com/robot/send')
|
|
50
|
+
uri.query = URI.encode_www_form(access_token: token)
|
|
51
|
+
# 第二次 JSON 编码只为 curl 配置中的双引号和反斜杠转义,正文仍为原始 JSON。
|
|
52
|
+
configuration = "url = #{JSON.generate(uri.to_s)}\ndata-binary = #{JSON.generate(JSON.generate(payload))}\n"
|
|
53
|
+
output, _stderr, status = Open3.capture3(
|
|
54
|
+
'/usr/bin/curl', '--disable', '--silent', '--show-error',
|
|
55
|
+
'--connect-timeout', '5', '--max-time', '15',
|
|
56
|
+
'--header', 'Content-Type: application/json; charset=utf-8',
|
|
57
|
+
'--write-out', "\n%{http_code}", '--config', '-', stdin_data: configuration
|
|
58
|
+
)
|
|
59
|
+
unless status.success?
|
|
60
|
+
UI.important("钉钉通知网络请求失败(curl exit=#{status.exitstatus}),继续原流程")
|
|
61
|
+
return nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
body, _separator, code = output.rpartition("\n")
|
|
65
|
+
{ code: Integer(code, 10), body: body }
|
|
66
|
+
end
|
|
67
|
+
private_class_method :post_message
|
|
33
68
|
end
|
|
34
69
|
end
|
|
35
70
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'fastlane_core/ui/ui'
|
|
7
|
+
|
|
8
|
+
module Fastlane
|
|
9
|
+
module Helper
|
|
10
|
+
# 二维码在构建机下载后内嵌到 Markdown,避免钉钉客户端再次访问分发服务器。
|
|
11
|
+
class DingdingImageHelper
|
|
12
|
+
# Base64 会增加约三分之一体积,为通知正文和更新说明留出空间。
|
|
13
|
+
MAX_IMAGE_BYTES = 8 * 1024
|
|
14
|
+
|
|
15
|
+
def self.base64_source(url)
|
|
16
|
+
uri = URI.parse(url.to_s)
|
|
17
|
+
return unless %w[http https].include?(uri.scheme) && uri.host && !uri.userinfo
|
|
18
|
+
|
|
19
|
+
data = download_image(uri)
|
|
20
|
+
return unless data
|
|
21
|
+
|
|
22
|
+
mime_type = image_mime_type(data)
|
|
23
|
+
unless mime_type
|
|
24
|
+
UI.important('二维码响应不是 PNG、JPEG 或 GIF 图片,通知保留图片链接')
|
|
25
|
+
return nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
"data:#{mime_type};base64,#{Base64.strict_encode64(data)}"
|
|
29
|
+
rescue StandardError => error
|
|
30
|
+
# 不输出 URL、响应正文或异常原文,避免签名参数进入打包日志。
|
|
31
|
+
UI.important("二维码内嵌失败(#{error.class}),通知保留图片链接")
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.download_image(uri)
|
|
36
|
+
Tempfile.create(['fastci-dingding-qr', '.image']) do |file|
|
|
37
|
+
# 不经过 shell;图片地址仅通过 stdin 传递,保留证书校验并限制下载量与耗时。
|
|
38
|
+
_output, _stderr, status = Open3.capture3(
|
|
39
|
+
'/usr/bin/curl', '--disable', '--silent', '--show-error', '--fail',
|
|
40
|
+
'--location', '--max-redirs', '3', '--proto', '=http,https', '--proto-redir', '=http,https',
|
|
41
|
+
'--connect-timeout', '5', '--max-time', '15', '--max-filesize', MAX_IMAGE_BYTES.to_s,
|
|
42
|
+
'--output', file.path, '--config', '-', stdin_data: "url = #{JSON.generate(uri.to_s)}\n"
|
|
43
|
+
)
|
|
44
|
+
unless status.success?
|
|
45
|
+
UI.important("二维码下载失败(curl exit=#{status.exitstatus}),通知保留图片链接")
|
|
46
|
+
return nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 额外检查实际字节数,兼容未返回 Content-Length 的图片接口。
|
|
50
|
+
data = File.binread(file.path, MAX_IMAGE_BYTES + 1)
|
|
51
|
+
if data.empty? || data.bytesize > MAX_IMAGE_BYTES
|
|
52
|
+
UI.important('二维码为空或超过 8 KB,通知保留图片链接')
|
|
53
|
+
return nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
data
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 按文件签名识别格式,避免把登录页或错误 HTML 当作图片编码发送。
|
|
61
|
+
def self.image_mime_type(data)
|
|
62
|
+
return 'image/png' if data.start_with?("\x89PNG\r\n\x1a\n".b)
|
|
63
|
+
return 'image/jpeg' if data.start_with?("\xff\xd8\xff".b)
|
|
64
|
+
return 'image/gif' if data.start_with?('GIF87a', 'GIF89a')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private_class_method :download_image, :image_mime_type
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -120,6 +120,19 @@ module Fastlane
|
|
|
120
120
|
ENV['DINGDING_TOKEN']
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# 商店安装入口由项目配置;上传接口本身不提供 TestFlight 邀请或线上商店地址。
|
|
124
|
+
def self.dingding_testflight_url
|
|
125
|
+
ENV['DINGDING_TESTFLIGHT_URL']
|
|
126
|
+
end
|
|
127
|
+
def self.dingding_app_store_url
|
|
128
|
+
ENV['DINGDING_APP_STORE_URL']
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# 默认兼容原有密码展示,配置 false 后不在群通知中附带安装密码。
|
|
132
|
+
def self.dingding_include_install_password
|
|
133
|
+
ENV.fetch('DINGDING_INCLUDE_INSTALL_PASSWORD', 'true').casecmp('true').zero?
|
|
134
|
+
end
|
|
135
|
+
|
|
123
136
|
# Sentry 配置
|
|
124
137
|
def self.sentry_auth_token
|
|
125
138
|
ENV['SENTRY_AUTH_TOKEN']
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
require 'cgi'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'fastlane/plugin/fastci/helper/dingding_helper'
|
|
4
|
+
require 'fastlane/plugin/fastci/helper/dingding_image_helper'
|
|
5
|
+
|
|
6
|
+
module Fastlane
|
|
7
|
+
module Helper
|
|
8
|
+
# 每次 package 独立保存通知上下文,避免多个 lane 之间串用版本或分发结果。
|
|
9
|
+
class PackageNotificationHelper
|
|
10
|
+
PLATFORM_NAMES = {
|
|
11
|
+
local: '本地构建', pgy: '蒲公英', fir: 'fir', zealot: 'Zealot',
|
|
12
|
+
testflight: 'TestFlight', app_store: 'App Store', checks: '代码检查', sentry: 'Sentry'
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def initialize(params)
|
|
16
|
+
@enabled = params[:is_notice_dingding] != false
|
|
17
|
+
@scheme = params[:scheme] || Environment.scheme
|
|
18
|
+
@export_method = params[:export_method] || 'development'
|
|
19
|
+
@configuration = %w[app-store testFlight].include?(@export_method) ? 'Release' : (params[:configuration] || 'Debug')
|
|
20
|
+
@version, @build = params[:version], params[:build]
|
|
21
|
+
@changelog = params[:changelog]
|
|
22
|
+
@started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
23
|
+
stage(:local, '准备构建')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# 在构建前记录实际版本,失败通知也能定位到本次产物。
|
|
27
|
+
def build_info(version:, build:)
|
|
28
|
+
@version, @build = version, build
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stage(platform, name)
|
|
32
|
+
@platform, @stage = platform, name
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# 各渠道只转换自己的返回字段,公共标题与正文始终使用同一模板。
|
|
36
|
+
def distribution_completed(platform, result = {}, store_result: {})
|
|
37
|
+
stage(platform, '上传分发包')
|
|
38
|
+
if !result.is_a?(Hash) || result.empty?
|
|
39
|
+
return notify('上传未完成', details: { '说明' => '上传接口未返回成功结果,请查看构建日志。' }, failed: true)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
fields = distribution_fields(platform, result, store_result)
|
|
43
|
+
notify('上传完成', **fields)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def store_skipped(platform)
|
|
47
|
+
stage(platform, '上传分发包')
|
|
48
|
+
notify('未上传', details: { '说明' => 'IPA 已生成,未配置商店上传认证。' })
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def checks_completed(names)
|
|
52
|
+
return if names.empty?
|
|
53
|
+
|
|
54
|
+
stage(:checks, '代码检查')
|
|
55
|
+
notify('检查执行完成', details: {
|
|
56
|
+
'已执行' => names.join('、'), '说明' => '检查已执行,发现的问题请查看本次生成的报告。'
|
|
57
|
+
})
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 失败消息只说明阶段,不把可能含凭据、个人路径的原始异常发送到群里。
|
|
61
|
+
def failure(store_result: {})
|
|
62
|
+
details = { '失败阶段' => @stage, '说明' => '本次流程已中断,请查看构建日志。' }
|
|
63
|
+
if [:testflight, :app_store].include?(@platform) && !store_result.empty?
|
|
64
|
+
details['失败阶段'] = store_result[:stage]
|
|
65
|
+
details['IPA'] = store_result[:ipa_uploaded] ? '已上传' : '尚未确认上传完成'
|
|
66
|
+
end
|
|
67
|
+
notify('执行失败', details: details, failed: true)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def distribution_fields(platform, result, store_result)
|
|
73
|
+
case platform
|
|
74
|
+
when :pgy
|
|
75
|
+
{
|
|
76
|
+
links: { '安装页面' => value(result, :fastlaneAddedWholeVisitUrl) },
|
|
77
|
+
qr_code: value(result, :buildQRCodeURL), password: Environment.pgy_password
|
|
78
|
+
}
|
|
79
|
+
when :fir
|
|
80
|
+
{ links: { '安装页面' => value(result, :download_url) }, password: Environment.fir_password }
|
|
81
|
+
when :zealot
|
|
82
|
+
{
|
|
83
|
+
links: { '版本详情' => value(result, :release_url), '安装页面' => value(result, :install_url) },
|
|
84
|
+
qr_code: value(result, :qrcode_url),
|
|
85
|
+
details: { 'Release ID' => value(result, :release_id), 'dSYM' => value(result, :dsym_status) || '未开启' }
|
|
86
|
+
}
|
|
87
|
+
when :testflight
|
|
88
|
+
{
|
|
89
|
+
links: store_links.merge('TestFlight 测试邀请' => Environment.dingding_testflight_url),
|
|
90
|
+
details: { '说明' => 'IPA 已上传,尚未等待 Apple 处理完成;测试可用性请在 App Store Connect 确认。' }
|
|
91
|
+
}
|
|
92
|
+
when :app_store
|
|
93
|
+
{ links: store_links.merge('App Store 线上页面' => Environment.dingding_app_store_url), details: store_details(store_result) }
|
|
94
|
+
else
|
|
95
|
+
{}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def store_links
|
|
100
|
+
{ 'App Store Connect' => 'https://appstoreconnect.apple.com/apps' }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# 使用上传流程确认过的结果,不从配置推断资源已上传或审核已通过。
|
|
104
|
+
def store_details(result)
|
|
105
|
+
details = { '说明' => '上传完成不代表审核通过或已上线;线上页面可能仍为旧版本。' }
|
|
106
|
+
unless result[:resources_enabled]
|
|
107
|
+
return details.merge('商店资源' => '未开启,仅上传 IPA')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
details.merge(
|
|
111
|
+
'元数据' => result[:metadata_uploaded] ? '已上传' : '未上传',
|
|
112
|
+
'截图' => result[:screenshots_uploaded] ? '已上传' : '未上传',
|
|
113
|
+
'构建选择' => result[:build_selected] ? '已选择本次 Build' : '未执行',
|
|
114
|
+
'提交审核' => result[:review_submitted] ? '已提交' : '未提交'
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def value(result, key)
|
|
119
|
+
result[key] || result[key.to_s]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# 不写空链接、空密码;安装密码开关默认保留旧版的展示行为。
|
|
123
|
+
def notify(status, details: {}, links: {}, qr_code: nil, password: nil, failed: false)
|
|
124
|
+
return unless @enabled && !Environment.dingdingToken.to_s.strip.empty?
|
|
125
|
+
|
|
126
|
+
platform = PLATFORM_NAMES.fetch(@platform)
|
|
127
|
+
title = "#{@scheme} iOS · #{platform} · #{status}"
|
|
128
|
+
fields = {
|
|
129
|
+
'应用' => @scheme, 'Bundle ID' => Environment.bundleID, '分发平台' => platform,
|
|
130
|
+
'状态' => status, '版本' => @version, 'Build' => @build,
|
|
131
|
+
'打包方式' => @export_method, '构建配置' => @configuration
|
|
132
|
+
}.merge(details)
|
|
133
|
+
if Environment.dingding_include_install_password
|
|
134
|
+
fields['安装密码'] = password
|
|
135
|
+
end
|
|
136
|
+
fields['累计耗时'] = "#{(Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at).round} 秒"
|
|
137
|
+
fields['通知时间'] = Time.now.strftime('%Y-%m-%d %H:%M:%S %:z')
|
|
138
|
+
sections = ["### #{failed ? '❌' : '📦'} #{markdown(title)}"]
|
|
139
|
+
sections << fields.map { |key, val| "- **#{key}**:#{markdown(val)}" unless val.to_s.strip.empty? }.compact.join("\n")
|
|
140
|
+
sections.concat(link_sections(links, qr_code))
|
|
141
|
+
unless @changelog.to_s.strip.empty?
|
|
142
|
+
# 为长更新日志留出消息空间;截断的是展示文案,不影响平台上传内容。
|
|
143
|
+
notes = @changelog.to_s[0, 1000]
|
|
144
|
+
notes += '…(更新说明已截断)' if @changelog.to_s.length > 1000
|
|
145
|
+
# 普通换行会被 Markdown 合并成段落;两个行尾空格明确要求换行。
|
|
146
|
+
# 仅格式化通知展示,兼容 CRLF,平台上传继续使用原始 changelog。
|
|
147
|
+
formatted_notes = markdown(notes).gsub(/\r\n?/, "\n").gsub("\n", " \n")
|
|
148
|
+
sections << "**更新说明**\n\n#{formatted_notes}"
|
|
149
|
+
end
|
|
150
|
+
DingdingHelper.sendMarkdown(sections.join("\n\n"), title: title)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def link_sections(links, qr_code)
|
|
154
|
+
sections = links.map do |label, url|
|
|
155
|
+
safe = web_url(url)
|
|
156
|
+
"[#{label}](#{safe})" if safe
|
|
157
|
+
end.compact
|
|
158
|
+
qr_url = web_url(qr_code)
|
|
159
|
+
if qr_url
|
|
160
|
+
image_source = DingdingImageHelper.base64_source(qr_url)
|
|
161
|
+
sections << "**扫码安装**\n\n" if image_source
|
|
162
|
+
sections << "[打开二维码](#{qr_url})"
|
|
163
|
+
end
|
|
164
|
+
sections
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# 仅允许普通网页地址,避免返回值中的括号或凭据破坏 Markdown 链接。
|
|
168
|
+
def web_url(value)
|
|
169
|
+
url = value.to_s.strip
|
|
170
|
+
uri = URI.parse(url)
|
|
171
|
+
return unless %w[https http].include?(uri.scheme) && uri.host && !uri.userinfo
|
|
172
|
+
return if url.match?(/[\s<>]/)
|
|
173
|
+
|
|
174
|
+
url.gsub('(', '%28').gsub(')', '%29')
|
|
175
|
+
rescue URI::InvalidURIError
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def markdown(value)
|
|
180
|
+
CGI.escapeHTML(value.to_s).gsub(/[\\`*_\[\]#]/) { |char| "\\#{char}" }
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fastlane-plugin-fastci
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.41
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- watermelon
|
|
@@ -107,9 +107,11 @@ files:
|
|
|
107
107
|
- lib/fastlane/plugin/fastci/helper/common_helper.rb
|
|
108
108
|
- lib/fastlane/plugin/fastci/helper/constants.rb
|
|
109
109
|
- lib/fastlane/plugin/fastci/helper/dingding_helper.rb
|
|
110
|
+
- lib/fastlane/plugin/fastci/helper/dingding_image_helper.rb
|
|
110
111
|
- lib/fastlane/plugin/fastci/helper/environment.rb
|
|
111
112
|
- lib/fastlane/plugin/fastci/helper/fastci_zealot_helper.rb
|
|
112
113
|
- lib/fastlane/plugin/fastci/helper/fir_helper.rb
|
|
114
|
+
- lib/fastlane/plugin/fastci/helper/package_notification_helper.rb
|
|
113
115
|
- lib/fastlane/plugin/fastci/version.rb
|
|
114
116
|
- lib/fastlane/plugin/python/generate_duplicity_code_html.py
|
|
115
117
|
- lib/fastlane/plugin/python/generate_lint_html.py
|