pindo 5.20.0 → 5.20.1

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.
@@ -28,6 +28,9 @@ module Pindo
28
28
  def initialize(config_file: nil)
29
29
  @force_login = false
30
30
  @has_login = false
31
+ # 项目列表分页进度:下一个待拉取的页码、是否已拉完所有页
32
+ @projects_next_page = 1
33
+ @projects_fully_loaded = false
31
34
  # PgyerHelper 提供默认配置路径
32
35
  @config_file = config_file || File.join(File::expand_path(Pindoconfig.instance.pindo_common_configdir), "jps_client_config.json")
33
36
  end
@@ -177,7 +180,7 @@ module Pindo
177
180
  # @param working_directory [String] 工作目录
178
181
  # @param manage_type [String] 管理类型(默认为空,"git" 时筛选 git 工作流)
179
182
  # @return [Hash] 选择的工作流信息
180
- def select_workflow_for_project(project_id:, package_type:, working_directory:, manage_type: "")
183
+ def select_workflow_for_project(project_id:, package_type:, working_directory:, manage_type: "", workflow_name: nil)
181
184
  # 1. 从 JPS API 获取可用工作流列表
182
185
  Funlog.instance.fancyinfo_start("正在获取可用工作流...")
183
186
 
@@ -241,8 +244,23 @@ module Pindo
241
244
  else package_type
242
245
  end
243
246
 
244
- # 非 git 模式下,如果过滤后没有结果,使用所有工作流
245
- display_workflows = filtered_workflows.empty? ? workflows : filtered_workflows
247
+ # 非 git 模式:过滤结果即为候选。若为空则交由下方“没有找到匹配工作流”报错,
248
+ # 不再回退到全部工作流——否则会把其它类型的工作流误当作请求类型(写错 workflow_key)
249
+ display_workflows = filtered_workflows
250
+ end
251
+
252
+ # 2.5 若指定了 workflow_name,按名称精确匹配(忽略大小写与首尾空格)确定性选择。
253
+ # 只接受完整精确匹配,不做子串模糊匹配,避免 "Production" 误命中 "Legacy Production Upload"。
254
+ if workflow_name && !workflow_name.to_s.strip.empty? && !display_workflows.empty?
255
+ key_name = workflow_name.to_s.strip.downcase
256
+ name_matched = display_workflows.select do |w|
257
+ (w['tabName'] || w['tab_name']).to_s.strip.downcase == key_name
258
+ end
259
+ if name_matched.empty?
260
+ available = display_workflows.map { |w| w['tabName'] || w['tab_name'] }.join('、')
261
+ raise Informative, "未找到名称精确匹配 \"#{workflow_name}\" 的 #{type_desc} 工作流(需与 tab 名完全一致),可选: #{available}"
262
+ end
263
+ display_workflows = name_matched
246
264
  end
247
265
 
248
266
  # 3. 根据匹配数量决定选择方式
@@ -463,7 +481,7 @@ module Pindo
463
481
  return app_info_obj, workflow_info
464
482
  end
465
483
 
466
- def prepare_upload(working_directory:nil, conf:nil, package_type:nil, manage_type: "", bundle_name: nil)
484
+ def prepare_upload(working_directory:nil, conf:nil, package_type:nil, manage_type: "", bundle_name: nil, proj_name: nil, workflow_name: nil)
467
485
  working_directory ||= Dir.pwd
468
486
 
469
487
  if ENV['PINDO_DEBUG']
@@ -474,6 +492,12 @@ module Pindo
474
492
  puts " manage_type: #{manage_type.inspect}"
475
493
  end
476
494
 
495
+ # 是否需要选择/保存工作流:空串 package_type/manage_type 视为“无工作流需求”(如 download 仅要项目)
496
+ wants_workflow = !package_type.to_s.strip.empty? || !manage_type.to_s.strip.empty?
497
+ # 是否显式指定了项目名(--proj)/工作流名(--workflow_name):命令行参数优先于已有配置
498
+ has_explicit_proj = !proj_name.to_s.strip.empty?
499
+ has_explicit_workflow_name = !workflow_name.to_s.strip.empty?
500
+
477
501
  # 确定配置文件路径:显式指定 > 工作目录自动检测 > 工程目录自动检测
478
502
  config_path = if conf && !conf.empty?
479
503
  File.expand_path(conf)
@@ -483,8 +507,19 @@ module Pindo
483
507
 
484
508
  puts "[PINDO_DEBUG] 使用配置文件: #{config_path}" if ENV['PINDO_DEBUG']
485
509
 
486
- # 尝试从配置文件加载
487
- if package_type || manage_type
510
+ # 显式 --workflow_name 但未给 --proj 时,从已有配置回填项目名,
511
+ # 使 --workflow_name 能在正确项目内按名重选工作流,而不是被旧配置里的工作流静默覆盖
512
+ if wants_workflow && has_explicit_workflow_name && !has_explicit_proj && File.exist?(config_path)
513
+ existing_cfg = (JSON.parse(File.read(config_path)) rescue nil)
514
+ existing_proj = existing_cfg && existing_cfg['project_name']
515
+ if existing_proj && !existing_proj.to_s.strip.empty?
516
+ proj_name = existing_proj
517
+ has_explicit_proj = true
518
+ end
519
+ end
520
+
521
+ # 尝试从配置文件加载(显式 --proj/--workflow_name 时跳过,保证命令行参数优先于已有配置)
522
+ if wants_workflow && !has_explicit_proj && !has_explicit_workflow_name
488
523
  config_result = parse_jps_build_config(
489
524
  config_file: config_path,
490
525
  package_type: package_type,
@@ -539,29 +574,35 @@ module Pindo
539
574
  app_info_obj = nil
540
575
  if login
541
576
 
542
- if !app_info_obj.nil?
543
- # 如果提供了 package_type 或 manage_type,选择工作流并保存配置
544
- if package_type || manage_type
545
- workflow_info = select_workflow_for_project(
546
- project_id: app_info_obj["id"],
547
- package_type: package_type,
548
- working_directory: working_directory,
549
- manage_type: manage_type
550
- )
551
-
552
- save_jps_build_config(
553
- working_directory: working_directory,
554
- app_info_obj: app_info_obj,
555
- workflow_info: workflow_info,
556
- package_type: package_type,
557
- manage_type: manage_type
558
- )
559
-
560
- @proj_name = upload_proj_name
561
- return app_info_obj, workflow_info
577
+ # 非交互:显式指定项目名(--proj)时直接解析,跳过项目选择菜单
578
+ if proj_name && !proj_name.to_s.strip.empty?
579
+ resolved_proj_name = proj_name.to_s.strip
580
+ app_info_obj = find_app_info_with_obj_list(proj_name: resolved_proj_name, strict: true)
581
+ if app_info_obj.nil?
582
+ raise Informative, "JPS 上未找到项目: #{resolved_proj_name},请检查项目名称"
583
+ end
584
+ PindoUserLocalConfig.instance.write_last_work_project(proj_name: app_info_obj["projectName"])
585
+ @proj_name = resolved_proj_name
586
+
587
+ if wants_workflow
588
+ workflow_info = select_workflow_for_project(
589
+ project_id: app_info_obj["id"],
590
+ package_type: package_type,
591
+ working_directory: working_directory,
592
+ manage_type: manage_type,
593
+ workflow_name: workflow_name
594
+ )
595
+ save_jps_build_config(
596
+ working_directory: working_directory,
597
+ app_info_obj: app_info_obj,
598
+ workflow_info: workflow_info,
599
+ package_type: package_type,
600
+ manage_type: manage_type,
601
+ conf: conf
602
+ )
603
+ return app_info_obj, workflow_info
562
604
  else
563
- @proj_name = upload_proj_name
564
- return app_info_obj
605
+ return app_info_obj
565
606
  end
566
607
  end
567
608
 
@@ -662,13 +703,14 @@ module Pindo
662
703
  # 记录上次上传的项目
663
704
  PindoUserLocalConfig.instance.write_last_work_project(proj_name:app_info_obj["projectName"])
664
705
 
665
- # 如果提供了 package_type 或 manage_type,选择工作流并保存配置
666
- if package_type || manage_type
706
+ # 如果需要工作流,选择工作流并保存配置
707
+ if wants_workflow
667
708
  workflow_info = select_workflow_for_project(
668
709
  project_id: app_info_obj["id"],
669
710
  package_type: package_type,
670
711
  working_directory: working_directory,
671
- manage_type: manage_type
712
+ manage_type: manage_type,
713
+ workflow_name: workflow_name
672
714
  )
673
715
 
674
716
  save_jps_build_config(
@@ -676,7 +718,8 @@ module Pindo
676
718
  app_info_obj: app_info_obj,
677
719
  workflow_info: workflow_info,
678
720
  package_type: package_type,
679
- manage_type: manage_type
721
+ manage_type: manage_type,
722
+ conf: conf
680
723
  )
681
724
 
682
725
  @proj_name = upload_proj_name
@@ -1357,41 +1400,108 @@ module Pindo
1357
1400
  result&.dig("data")
1358
1401
  end
1359
1402
 
1360
- def find_app_info_with_obj_list(proj_name:nil, bundle_id:nil)
1403
+ PROJECT_PAGE_SIZE = 100
1404
+ # 安全上限:最多翻 200 页(约 2 万项目),防止服务端异常(如忽略 pageNo 恒返满页)导致死循环
1405
+ MAX_PROJECT_PAGES = 200
1406
+
1407
+ def find_app_info_with_obj_list(proj_name:nil, bundle_id:nil, strict: false)
1361
1408
 
1362
1409
  if proj_name.nil? && bundle_id.nil?
1363
1410
  return nil
1364
1411
  end
1365
1412
 
1366
- Funlog.instance.fancyinfo_start("正在拉取app信息列表,查找app信息...")
1367
-
1368
- get_app_list_in_pgyer()
1413
+ Funlog.instance.fancyinfo_start("正在查找 JPS 项目...")
1369
1414
 
1370
1415
  key_proj_name = proj_name&.downcase&.strip&.gsub(/[\s\-_]/, '')
1371
1416
  key_bundle_id = bundle_id&.downcase&.strip
1372
1417
 
1373
- app_info_list = Pindoconfig.instance.get_pgyerapps_info_list()
1374
- app_info_obj = nil
1375
- if key_bundle_id && !key_bundle_id.empty?
1376
- app_info_obj = app_info_list&.find do |app_info_item|
1377
- app_info_bundle_id = app_info_item["bundleId"].to_s.downcase.strip
1378
- app_info_bundle_id.eql?(key_bundle_id)
1418
+ # 在给定列表中查找匹配对象:bundle_id 精确优先,其次项目名归一化匹配。
1419
+ # strict 且归一化撞到多个时优先完整精确匹配,仍多个则报错要求精确名。
1420
+ matcher = lambda do |list|
1421
+ obj = nil
1422
+ if key_bundle_id && !key_bundle_id.empty?
1423
+ obj = list.find { |it| it["bundleId"].to_s.downcase.strip.eql?(key_bundle_id) }
1379
1424
  end
1380
- end
1381
-
1382
- if app_info_obj.nil? && key_proj_name && !key_proj_name.empty?
1383
- app_info_obj = app_info_list&.find do |app_info_item|
1384
- # API 使用 projectName 字段
1385
- app_info_item_name = app_info_item["projectName"].to_s.downcase.strip.gsub(/[\s\-_]/, '')
1386
- app_info_item_name.eql?(key_proj_name)
1425
+ if obj.nil? && key_proj_name && !key_proj_name.empty?
1426
+ matches = list.select do |it|
1427
+ it["projectName"].to_s.downcase.strip.gsub(/[\s\-_]/, '').eql?(key_proj_name)
1428
+ end
1429
+ if strict && matches.size > 1
1430
+ key_exact = proj_name.to_s.downcase.strip
1431
+ exact = matches.select { |it| it["projectName"].to_s.downcase.strip.eql?(key_exact) }
1432
+ if exact.size == 1
1433
+ obj = exact.first
1434
+ else
1435
+ names = matches.map { |it| it["projectName"] }.join('、')
1436
+ raise Informative, "项目名 \"#{proj_name}\" 匹配到多个 JPS 项目: #{names},请使用精确名称"
1437
+ end
1438
+ else
1439
+ obj = matches.first
1440
+ end
1387
1441
  end
1442
+ obj
1388
1443
  end
1389
1444
 
1390
- Funlog.instance.fancyinfo_success("拉取app信息列表完成,app信息获取成功!")
1445
+ # strict 需完整列表才能判定唯一;其余边拉边找、命中即早停
1446
+ list = ensure_projects_loaded(require_full: strict) { |acc| !matcher.call(acc).nil? }
1447
+ app_info_obj = matcher.call(list)
1448
+
1449
+ Funlog.instance.fancyinfo_success("JPS 项目查找完成")
1391
1450
 
1392
1451
  app_info_obj
1393
1452
  end
1394
1453
 
1454
+ # 逐页(每页 PROJECT_PAGE_SIZE=100)拉取 JPS 项目列表并累积到进程缓存。
1455
+ # - require_full=false:每拉一页调用 stop_when(累积列表),命中即停止继续拉取(早停)
1456
+ # - require_full=true :拉完所有页(strict 判唯一等需要完整列表的场景)
1457
+ # 已缓存的页会复用并从上次进度续拉;全部页拉完仍未命中即返回完整列表,由调用方判定“不存在”。
1458
+ #
1459
+ # @param require_full [Boolean] 是否必须拉完所有页
1460
+ # @yield [Array] 当前累积的项目列表;返回真值表示已命中,可停止续拉
1461
+ # @return [Array] 累积到的项目列表
1462
+ def ensure_projects_loaded(require_full: false, &stop_when)
1463
+ accumulated = Pindoconfig.instance.get_pgyerapps_info_list() || []
1464
+ return accumulated if @projects_fully_loaded
1465
+ if !require_full && stop_when && stop_when.call(accumulated)
1466
+ return accumulated
1467
+ end
1468
+
1469
+ page_no = @projects_next_page ||= 1
1470
+ loop do
1471
+ if page_no > MAX_PROJECT_PAGES
1472
+ Funlog.instance.fancyinfo_warning("项目分页已达安全上限 #{MAX_PROJECT_PAGES} 页,停止继续拉取")
1473
+ @projects_fully_loaded = true
1474
+ break
1475
+ end
1476
+
1477
+ res = @pgyer_client.get_project_list(params: { pageNo: page_no, pageSize: PROJECT_PAGE_SIZE })
1478
+ data = res && res["data"]
1479
+
1480
+ if data.nil?
1481
+ # 首页即无数据 → 网络/登录异常;非首页则视为已到末尾
1482
+ if page_no == 1 && accumulated.empty?
1483
+ err = res && (res["msg"] || res["code"])
1484
+ raise Informative, "JPS网络数据异常或未登录,请检查网络或重新登录(pindo jps login)#{err ? ":#{err}" : ""}"
1485
+ end
1486
+ @projects_fully_loaded = true
1487
+ break
1488
+ end
1489
+
1490
+ accumulated += data
1491
+ Pindoconfig.instance.set_pgyerapps_info_list(app_info_list: accumulated)
1492
+ @projects_next_page = (page_no += 1)
1493
+
1494
+ if data.size < PROJECT_PAGE_SIZE
1495
+ # 不足一页 → 已是最后一页
1496
+ @projects_fully_loaded = true
1497
+ break
1498
+ end
1499
+ break if !require_full && stop_when && stop_when.call(accumulated)
1500
+ end
1501
+
1502
+ accumulated
1503
+ end
1504
+
1395
1505
  def wildcard_android_bundle_name?(bundle_name)
1396
1506
  return false if bundle_name.nil? || bundle_name.empty?
1397
1507
 
@@ -1438,27 +1548,11 @@ module Pindo
1438
1548
  return version_item_obj
1439
1549
  end
1440
1550
 
1551
+ # 拉取(累积缓存中的)全部 JPS 项目列表——分页拉完所有页,去除旧的单页 1000 上限。
1441
1552
  def get_app_list_in_pgyer()
1553
+ app_info_list = ensure_projects_loaded(require_full: true)
1442
1554
 
1443
- params = {
1444
- pageNo:1,
1445
- pageSize:1000
1446
- }
1447
-
1448
- app_info_list = Pindoconfig.instance.get_pgyerapps_info_list()
1449
- if app_info_list.nil?
1450
- res_data = @pgyer_client.get_project_list(params:params)
1451
- if res_data && !res_data["data"].nil? && res_data["data"].size > 0
1452
- app_info_list = res_data["data"]
1453
- Pindoconfig.instance.set_pgyerapps_info_list(app_info_list:app_info_list)
1454
- elsif res_data
1455
- error_msg = res_data["msg"] || "未知错误"
1456
- error_code = res_data["code"]
1457
- Funlog.instance.fancyinfo_error("拉取项目列表失败: [#{error_code}] #{error_msg}")
1458
- end
1459
- end
1460
-
1461
- if app_info_list.nil?
1555
+ if app_info_list.nil? || app_info_list.empty?
1462
1556
  Funlog.instance.fancyinfo_error("拉取app信息列表失败!")
1463
1557
  raise Informative, "JPS网络数据异常,请检查网络连接或重新登录(pindo jps login)"
1464
1558
  end
@@ -1935,8 +2029,97 @@ module Pindo
1935
2029
  end
1936
2030
  end
1937
2031
 
2032
+ # 一次性把多个工作流写入 JPSBuildConfig.json(单次原子写盘),供 pindo jps config 多类型配置使用。
2033
+ # 同一项目下已有的其它工作流键会保留;项目变更则整份重建。写入是原子的,
2034
+ # 中途不会留下半成品配置。
2035
+ #
2036
+ # @param working_directory [String] 工作目录
2037
+ # @param app_info_obj [Hash] 已选定的 JPS 项目信息对象
2038
+ # @param entries [Array<Hash>] 形如 [{ package_type:, manage_type:, workflow_info: }]
2039
+ # @param conf [String, nil] 显式配置文件路径(--conf),优先于自动定位
2040
+ # @return [Hash, nil] 写入后的完整配置
2041
+ def save_jps_workflows(working_directory:, app_info_obj:, entries:, conf: nil)
2042
+ return if working_directory.nil? || app_info_obj.nil? || entries.nil? || entries.empty?
2043
+
2044
+ config_file = resolve_jps_config_file(working_directory: working_directory, conf: conf)
2045
+ new_project_id = app_info_obj["id"]
2046
+
2047
+ # 仅当项目 ID 相同才保留已有其它工作流键;否则整份重建
2048
+ config = {}
2049
+ if File.exist?(config_file)
2050
+ existing = (JSON.parse(File.read(config_file)) rescue nil)
2051
+ config = existing if existing.is_a?(Hash) && existing['project_id'] == new_project_id
2052
+ end
2053
+
2054
+ config['project_name'] = app_info_obj["projectName"]
2055
+ config['project_scheme'] = app_info_obj["scheme"] ||
2056
+ app_info_obj["projectName"].to_s.downcase.strip.gsub(/[\s\-_]/, '')
2057
+ config['project_id'] = new_project_id
2058
+ config['bundle_group_name'] = app_info_obj["bundleId"]
2059
+ config['wildcard_bundle_group_name'] = app_info_obj["wildcardBundleId"]
2060
+
2061
+ entries.each do |entry|
2062
+ workflow_key = jps_workflow_key(package_type: entry[:package_type], manage_type: entry[:manage_type].to_s)
2063
+ config[workflow_key] = build_workflow_data(
2064
+ workflow_info: entry[:workflow_info],
2065
+ package_type: entry[:package_type]
2066
+ )
2067
+ end
2068
+
2069
+ atomic_write(config_file, JSON.pretty_generate(config) + "\n")
2070
+ puts "已写入 JPSBuildConfig.json(#{entries.size} 个工作流)"
2071
+ config
2072
+ end
2073
+
1938
2074
  private
1939
2075
 
2076
+ # 原子写文件:写同目录临时文件再 rename,避免半写状态。
2077
+ # rename 会以普通文件替换符号链接(不写穿其目标),符合“不跟随符号链接”的预期。
2078
+ def atomic_write(path, content)
2079
+ dir = File.dirname(path)
2080
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
2081
+ tmp = File.join(dir, ".#{File.basename(path)}.#{Process.pid}.tmp")
2082
+ begin
2083
+ File.write(tmp, content)
2084
+ File.rename(tmp, path)
2085
+ ensure
2086
+ File.delete(tmp) if File.exist?(tmp)
2087
+ end
2088
+ end
2089
+
2090
+ # 定位 JPSBuildConfig.json:显式 --conf 优先,否则按工作目录自动定位
2091
+ def resolve_jps_config_file(working_directory:, conf: nil)
2092
+ if conf && !conf.to_s.strip.empty?
2093
+ File.expand_path(conf)
2094
+ else
2095
+ Pindo::BuildHelper.share_instance.determine_jps_config_path(working_directory)
2096
+ end
2097
+ end
2098
+
2099
+ # package_type/manage_type → JPSBuildConfig.json 中的 workflow_key
2100
+ def jps_workflow_key(package_type:, manage_type: "")
2101
+ return 'git_workflow' if manage_type == 'git'
2102
+ case package_type
2103
+ when 'ipa' then 'ipa_workflow'
2104
+ when 'apk' then 'apk_workflow'
2105
+ when 'zip' then 'webgl_workflow'
2106
+ when 'mac' then 'macos_workflow'
2107
+ when 'exe' then 'win_workflow'
2108
+ else raise Informative, "不支持的 package_type: #{package_type}"
2109
+ end
2110
+ end
2111
+
2112
+ # 构建单个工作流的写入结构
2113
+ def build_workflow_data(workflow_info:, package_type:)
2114
+ {
2115
+ "workflow_id" => workflow_info[:workflow_id],
2116
+ "tab_name" => workflow_info[:tab_name],
2117
+ "package_type" => package_type,
2118
+ "package_name" => workflow_info[:package_name],
2119
+ "manage_type" => workflow_info[:manage_type] || ""
2120
+ }
2121
+ end
2122
+
1940
2123
  # 验证工作流配置是否有效
1941
2124
  #
1942
2125
  # @param workflow [Hash] 工作流配置对象
@@ -1985,11 +2168,7 @@ module Pindo
1985
2168
  workflow_key => workflow_data
1986
2169
  }
1987
2170
 
1988
- # 确保目录存在
1989
- config_dir = File.dirname(config_file)
1990
- FileUtils.mkdir_p(config_dir) unless File.directory?(config_dir)
1991
-
1992
- File.write(config_file, JSON.pretty_generate(new_config))
2171
+ atomic_write(config_file, JSON.pretty_generate(new_config))
1993
2172
  puts "已创建 JPSBuildConfig.json"
1994
2173
  end
1995
2174
 
@@ -2000,28 +2179,14 @@ module Pindo
2000
2179
  # @param app_info_obj [Hash] 项目信息对象
2001
2180
  # @param workflow_info [Hash] 工作流信息
2002
2181
  # @param package_type [String] 包类型 ("ipa" | "apk" | "zip")
2003
- def save_jps_build_config(working_directory:, app_info_obj:, workflow_info:, package_type:, manage_type: "")
2182
+ def save_jps_build_config(working_directory:, app_info_obj:, workflow_info:, package_type:, manage_type: "", conf: nil)
2004
2183
  return if working_directory.nil? || app_info_obj.nil? || workflow_info.nil?
2005
2184
 
2006
- # 1. 确定配置文件路径
2007
- config_file = Pindo::BuildHelper.share_instance.determine_jps_config_path(working_directory)
2185
+ # 1. 确定配置文件路径:显式 --conf 优先,否则按工作目录自动定位
2186
+ config_file = resolve_jps_config_file(working_directory: working_directory, conf: conf)
2008
2187
 
2009
2188
  # 2. 根据 manage_type 或 package_type 确定 workflow_key
2010
- workflow_key = nil
2011
- if manage_type == "git"
2012
- # manage_type 为 git 时,使用 git_workflow
2013
- workflow_key = 'git_workflow'
2014
- else
2015
- # 按 package_type 确定 workflow_key
2016
- workflow_key = case package_type
2017
- when 'ipa' then 'ipa_workflow'
2018
- when 'apk' then 'apk_workflow'
2019
- when 'zip' then 'webgl_workflow'
2020
- when 'mac' then 'macos_workflow'
2021
- when 'exe' then 'win_workflow'
2022
- else raise Informative, "不支持的 package_type: #{package_type}"
2023
- end
2024
- end
2189
+ workflow_key = jps_workflow_key(package_type: package_type, manage_type: manage_type.to_s)
2025
2190
 
2026
2191
  # 3. 准备新的项目基础信息
2027
2192
  new_project_id = app_info_obj["id"]
@@ -2032,13 +2197,7 @@ module Pindo
2032
2197
  app_info_obj["projectName"].to_s.downcase.strip.gsub(/[\s\-_]/, '')
2033
2198
 
2034
2199
  # 4. 准备新的工作流信息
2035
- new_workflow_data = {
2036
- "workflow_id" => workflow_info[:workflow_id],
2037
- "tab_name" => workflow_info[:tab_name],
2038
- "package_type" => package_type,
2039
- "package_name" => workflow_info[:package_name],
2040
- "manage_type" => workflow_info[:manage_type] || ""
2041
- }
2200
+ new_workflow_data = build_workflow_data(workflow_info: workflow_info, package_type: package_type)
2042
2201
 
2043
2202
  # 5. 配置文件不存在 → 创建新配置
2044
2203
  if !File.exist?(config_file)
@@ -2105,7 +2264,7 @@ module Pindo
2105
2264
  existing_config[workflow_key] = new_workflow_data
2106
2265
  existing_config["bundle_group_name"] = new_bundle_group_name
2107
2266
  existing_config["wildcard_bundle_group_name"] = new_wildcard_bundle_group_name
2108
- File.write(config_file, JSON.pretty_generate(existing_config))
2267
+ atomic_write(config_file, JSON.pretty_generate(existing_config))
2109
2268
  puts "已更新 #{workflow_key}"
2110
2269
  return
2111
2270
  end
@@ -0,0 +1,83 @@
1
+ require 'pindo/module/build/build_helper'
2
+
3
+ module Pindo
4
+ # autobuild 会注入配置的固定源码清单。路径中的工程名、Target 名和 Module 名用 glob 匹配。
5
+ module BuildSourceFilesGuard
6
+ IOS_PATTERNS = [
7
+ 'config.json',
8
+ 'Podfile.lock',
9
+ '**/*.xcodeproj/project.pbxproj',
10
+ '**/Info.plist',
11
+ '**/*.entitlements',
12
+ '**/AppIcon.appiconset',
13
+ '**/iMessage App Icon.stickersiconset'
14
+ ].freeze
15
+
16
+ ANDROID_PATTERNS = [
17
+ 'build.gradle',
18
+ 'build.gradle.kts',
19
+ 'settings.gradle',
20
+ 'settings.gradle.kts',
21
+ 'google-services.json',
22
+ '**/build.gradle',
23
+ '**/build.gradle.kts',
24
+ '**/google-services.json',
25
+ '**/src/main/AndroidManifest.xml',
26
+ '**/src/main/res'
27
+ ].freeze
28
+
29
+ EXCLUDED_PATH_COMPONENTS = %w[
30
+ .git
31
+ .gradle
32
+ .build
33
+ Pods
34
+ build
35
+ DerivedData
36
+ GoodPlatform
37
+ ].freeze
38
+
39
+ class << self
40
+ def for_project(project_path)
41
+ case Pindo::BuildHelper.share_instance.project_type(project_path)
42
+ when :ios
43
+ ios(project_path)
44
+ when :android
45
+ android(project_path)
46
+ else
47
+ unity(project_path)
48
+ end
49
+ end
50
+
51
+ def ios(project_path)
52
+ source_paths(project_path, IOS_PATTERNS)
53
+ end
54
+
55
+ def android(project_path)
56
+ source_paths(project_path, ANDROID_PATTERNS)
57
+ end
58
+
59
+ # Unity 平台工程导出到已忽略的 GoodPlatform,不提交 Unity 源码配置。
60
+ def unity(_project_path)
61
+ []
62
+ end
63
+
64
+ private
65
+
66
+ def source_paths(project_path, patterns)
67
+ project_dir = File.expand_path(project_path)
68
+ project_prefix = "#{project_dir}#{File::SEPARATOR}"
69
+ candidates = patterns.flat_map { |pattern| Dir.glob(File.join(project_dir, pattern)) }
70
+
71
+ candidates
72
+ .select { |path| path.start_with?(project_prefix) }
73
+ .reject { |path| excluded_source_path?(project_dir, path) }
74
+ .uniq
75
+ end
76
+
77
+ def excluded_source_path?(project_dir, path)
78
+ relative_path = path.delete_prefix("#{project_dir}#{File::SEPARATOR}")
79
+ !(relative_path.split(File::SEPARATOR) & EXCLUDED_PATH_COMPONENTS).empty?
80
+ end
81
+ end
82
+ end
83
+ end
@@ -143,14 +143,14 @@ module Pindo
143
143
  )
144
144
 
145
145
  # 下载并替换 Icon
146
- if @app_info_obj && @app_info_obj["projectIcon"] && !@app_info_obj["projectIcon"].empty?
146
+ if @app_info_obj && @app_info_obj["packageProjectIcon"] && !@app_info_obj["packageProjectIcon"].empty?
147
147
  icon_replace_success = Pindo::XcodeBuildConfig.download_and_replace_icon_from_url(
148
148
  project_dir: @project_path,
149
- icon_url: @app_info_obj["projectIcon"]
149
+ icon_url: @app_info_obj["packageProjectIcon"]
150
150
  )
151
151
 
152
152
  unless icon_replace_success
153
- raise Informative, "Icon 下载或替换失败,请检查 Icon URL 是否有效: #{@app_info_obj["projectIcon"]}"
153
+ raise Informative, "Icon 下载或替换失败,请检查 Icon URL 是否有效: #{@app_info_obj["packageProjectIcon"]}"
154
154
  end
155
155
  else
156
156
  Funlog.warning("未配置 Icon URL,跳过 Icon 替换")
@@ -40,9 +40,8 @@ module Pindo
40
40
  @process_type = options[:process_type] || Pindo::UncommittedFilesProcessType::SKIP_WITH_TAG # 默认跳过但打tag
41
41
  @commit_message = options[:commit_message] || 'chore(build): pindo 构建产生提交' # 默认提交消息
42
42
 
43
- options[:project_path] = project_path
44
43
  # 调用基类初始化(基类会设置 release_branch, ver_inc, tag_type, tag_pre 等通用属性)
45
- super("Git仓库文件提交检查", options)
44
+ super("Git仓库文件提交检查", options.merge(project_path: project_path))
46
45
  end
47
46
 
48
47
  def validate
@@ -54,6 +53,13 @@ module Pindo
54
53
 
55
54
  protected
56
55
 
56
+ def build_task_param
57
+ param = super
58
+ return param if param.empty?
59
+
60
+ param.merge(process_type: @process_type)
61
+ end
62
+
57
63
  def do_work
58
64
  # 检查是否为 Git 仓库,如果不是则直接成功返回
59
65
  unless valid_git_repository?
@@ -159,6 +165,10 @@ module Pindo
159
165
  # 5. 获取当前分支
160
166
  coding_branch = get_current_branch_name
161
167
 
168
+ # 5.1 解析发布分支:仓库无 master 时按 master>main>唯一远程分支>当前分支 探测,
169
+ # 避免默认 'master' 与实际主分支(如 main)不一致导致同步/checkout 失败
170
+ @release_branch = Pindo::GitHandler.resolve_release_branch(local_repo_dir: root_dir, preferred: @release_branch)
171
+
162
172
  # 6. 检查并推送本地提交到远程(确保本地和远程同步)
163
173
  Pindo::GitHandler.check_unpushed_commits(project_dir: root_dir, branch: coding_branch)
164
174