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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5978da99fdc693c5d6f873bee51f791254b236e6eb92d9beaeea064ca730b52
4
- data.tar.gz: 70cc23a65e930ee3df7359a10ae751544276fb64b0b9dd1db395033e7a12322b
3
+ metadata.gz: 834603cf8a0a78c4fec22ee9f17d84a5f491d25770b2972c3e85b0b5ffcfcc27
4
+ data.tar.gz: a9ded28f219cd9215058581204ac445601479c0978b4e393f99ed0835f6fe461
5
5
  SHA512:
6
- metadata.gz: a4ec254de949a0c02e98985d29d2d77d958da26c6a4925ecc8c151c3d29c1faa3766a3f11e410d4211fc4c20b9cb3e91e45688958fe6b9ca2b3278f1f23cb6cb
7
- data.tar.gz: bc29f33a3c238d6aa61f7d9500c7eb97969b19c63231e321a8758b2adb7df4c7468b978a7fc5bc3a9aa5d516aefd96ca66b83924dcd6bf06145258202469dfc4
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.40` 后:
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.40 -m "Release 0.0.40"
426
- git push origin 0.0.40
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
- if gym_method == 'app-store'
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![screenshot](#{qrCode})"
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
- if is_analyze_swiftlint && gym_method != 'app-store'
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
- if is_detect_duplicity_code && gym_method != 'app-store'
216
- other_action.detect_duplicity_code(
217
- is_all: true
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
- if is_detect_unused_code && gym_method != 'app-store'
228
- other_action.detect_unused_code(
229
- scheme: scheme,
230
- is_from_package: true,
231
- configuration: configuration
232
- )
233
- # 结果复制到桌面
234
- FileUtils.cp(Constants.UNUSED_CODE_HTML_FILE, target_path)
235
- UI.message('*************| 无用代码检查完成 |*************')
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
- UI.message('*************| 跳过无用代码检查 |*************')
167
+ distribute_test_package(params, notice, ipa_path)
238
168
  end
169
+ end
239
170
 
240
- # 无用图片检查
241
- if is_detect_unused_image && gym_method != 'app-store'
242
- other_action.detect_unused_image
243
- # 结果复制到桌面
244
- FileUtils.cp(Constants.UNUSED_IMAGE_HTML_FILE, target_path)
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
- if is_analyze_swiftlint ||
251
- is_detect_duplicity_code ||
252
- is_detect_unused_code ||
253
- is_detect_unused_image
254
- # 钉钉通知
255
- notiText = "🚀🚀🚀🚀🚀🚀\n\n#{scheme}-iOS-代码检查完成\n\n#{version}_#{build}_#{export_method}\n\n🚀🚀🚀🚀🚀🚀"
256
- send_dingding_notice.call(notiText)
257
- else
258
- UI.message('*************| 跳过代码检查 |*************')
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
- UI.message('*************| 脚本完成 |*************')
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
- other_action.upload_to_testflight({ skip_waiting_for_build_processing: true }.merge(api_key_options))
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 'fileutils'
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
- def self.sendMarkdown(text)
9
-
10
- # 检查钉钉 Token 是否存在
11
- unless CommonHelper.is_validate_string(Environment.dingdingToken)
12
- UI.message("*************| 跳过钉钉消息通知(未配置 Token)|*************")
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
- UI.message("*************| 开始钉钉消息通知 |*************")
17
-
18
- curl = %Q{
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
- system curl
30
- UI.message("")
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![安装二维码](#{image_source})" 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
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Fastci
3
- VERSION = "0.0.40"
3
+ VERSION = "0.0.41"
4
4
  end
5
5
  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.40
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