├── .gitignore ├── .rubocop.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile └── fastlane ├── AndroidFastfile ├── Fastfile ├── actions ├── get_target_identifer.rb ├── get_xcode_configurations.rb ├── get_xcode_schemes.rb ├── git_last_commit.rb ├── git_remotes.rb ├── run_time.rb ├── service_health.rb ├── update_entitlements.rb ├── update_target_provisioning.rb ├── update_user_defined.rb ├── wechat_work.rb └── xcode_bootstrap.rb ├── helper └── xcode.rb └── iOSFastfile /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Metrics/LineLength: 2 | Max: 250 3 | 4 | Metrics/ClassLength: 5 | Max: 200 6 | 7 | Metrics/MethodLength: 8 | Max: 100 9 | 10 | Metrics/AbcSize: 11 | Max: 50 12 | 13 | Style/AsciiComments: 14 | Enabled: false 15 | 16 | Style/GlobalVars: 17 | Enabled: false 18 | 19 | Style/PredicateName: 20 | Enabled: false 21 | 22 | Style/BracesAroundHashParameters: 23 | EnforcedStyle: context_dependent -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://gems.ruby-china.org' 2 | # source 'https://rubygems.org' 3 | 4 | # Specify your gem's dependencies in fastlane-qyer.gemspec 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2015-present icyleaf 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastlane-plugins 2 | 3 | [![License](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/icyleaf/fastlane-plugins/blob/master/LICENSE) 4 | 5 | 本仓库是本人在多年所整理使用的超实用 [fastlane](http://github.com/fastlane/fastlane) 的自定义 actions,制定了一些用于公司内部移动开发项目使用到的 `lanes`,同时也有部分 action 在确定可以贡献社区的会被抽成 plugin 让更多的人检索和使用。 6 | 7 | ## 引入教程 8 | 9 | 确保已经安装了 `fastlane` 并进行初始化: 10 | 11 | ```bash 12 | $ gem install fastlane 13 | $ fastlane init 14 | ``` 15 | 16 | 根据工具的提示输入和配置你项目的具体参数和账户情况,直至完成之后会在当前路径下生成 `fastlane` 的目录结构,打开其中的 `Fastfile` 在头部加入并保存: 17 | 18 | ```ruby 19 | import_from_git(url: 'https://github.com/icyleaf/fastlane-plugins.git') 20 | ``` 21 | 22 | 接着我们就可以直接使用了! 23 | 24 | ## Plugins 25 | 26 | 如下是已经从本仓库移除并重新定义的 plugins 27 | 28 | 插件 | 说明 29 | ---|--- 30 | [ci_changelog](https://github.com/icyleaf/fastlane-plugin-ci_changelog) | 支持多种 CI 系统自动生成变更历史 31 | [update_jenkins_build](https://github.com/icyleaf/fastlane-plugin-update_jenkins_build) | 自动更新 Jenkins Build 描述 32 | [humanable_build_number](https://github.com/icyleaf/fastlane-plugin-humanable_build_number) | 生成开发可识别的构建版本号 33 | [app_info](https://github.com/icyleaf/fastlane-plugin-app_info) | 解析 apk/ipa 包的 metadata 并打印 34 | [android_channels](https://github.com/icyleaf/fastlane-plugin-android_channels) | 通用性 Android 多渠道打包 35 | [ram_disk](https://github.com/icyleaf/fastlane-plugin-ram_disk) | 创建内存虚拟磁盘,主要用于提升 App 构建速度 36 | [debug_file](https://github.com/icyleaf/fastlane-plugin-debug_file) | 自动化搜索 iOS/macOS dSYM 或 Android Proguard(混淆)并打包 Zip 文件 37 | 38 | ## Actions 39 | 40 | ### git_last_commit 41 | 42 | 默认是对 CI 且安装了 git 客户端的机器使用,主要用于获取当前拉取的 commit 的基本信息。 43 | 44 | ### git_remotes 45 | 46 | 和上面类似,只是获取当前 git 仓库的 remotes 信息 47 | 48 | ### wechat_works 49 | 50 | 使用企业微信的机器人 WebHook 发消息到群 51 | 52 | #### 参数 53 | 54 | 名称 | 环境变量 | 说明 | 默认值 | 55 | ---|---|---|--- 56 | webhook_url | WECHATWORK_WEBHOOK_URL | 企业微信机器人 webhook 57 | type | WECHATWORK_TYPE | 消息类型,可选值:`:text` 和 `:markdown` 58 | message | WECHATWORK_MESSAGEE | 消息内容 59 | to | WECHATWORK_TO | @ 某人的 id 或手机号、不支持昵称 60 | fail_on_error | WECHATWORK_FAIL_ON_ERROR | 运行遇到错误是否抱错退出 61 | 62 | #### 使用方法 63 | 64 | ```ruby 65 | lane :notice do 66 | # 发送纯文本消息 67 | wechat_work( 68 | webhook_url: '...', 69 | type: :text, 70 | message: "hello\nworld" 71 | ) 72 | 73 | # 发送 markdown 消息 74 | wechat_work( 75 | webhook_url: '...', 76 | type: :markdown, 77 | message: "# Head 1\n- List 1\n- List 2" 78 | ) 79 | end 80 | ``` 81 | 82 | ### xcode_bootstrap 83 | 84 | 每当新启动一个项目,在支持 fastlane 都需要花费时间和规范来配置参数,有了它可以轻松帮你一键配置如下的设置: 85 | 86 | - 支持 Cocoapods 87 | - 添加 **AdHoc** Build Confiuration 用于 fastlane 打包(自动处理 `Podfile` 文件) 88 | - 根据 Build Confiuration 区别安装应用的名称 89 | - Debug: {应用名}开发版(默认配置,可自定义) 90 | - AdHoc: {应用名}内测版(默认配置,可自定义) 91 | - Release: {应用名} 92 | - 根据 Build Confiuration 区别安装应用的 identifier 93 | - Debug: {应用identifier}.debug (默认配置,可自定义) 94 | - AdHoc: {应用identifier} 95 | - Release: {应用identifier} 96 | 97 | #### 参数 98 | 99 | 名称 | 环境变量 | 说明 | 默认值 | 100 | ---|---|---|--- 101 | project_path | `XCODE_PROJECT_PATH` | 项目路径 | 默认根目录 102 | cocoapods | `XCODE_COCOAPODS_SUPPORT` | 是否处理 Podfile | 默认 `true` 103 | build_configuration_name | `XCODE_BUILD_CONFIGURATION_NAME` | 新加编译配置名 | 默认 `AdHoc` 104 | build_configuration_base | `XCODE_BUILD_CONFIGURATION_BASE` | 新加编译配置的继承 | 默认 `:release` 105 | app_suffix | `XCODE_APP_SUFFIX` | 自定义应用名和唯一标识 | 默认参考上面说明 106 | 107 | #### 使用方法 108 | 109 | 在 `Fastfile` 添加你定义好的 lane: 110 | 111 | ```ruby 112 | # 默认配置(参考上面说明配置) 113 | lane :bootstrap do 114 | xcode_bootstrap 115 | end 116 | 117 | # 自定义 Build Confiugration 118 | lane :bootstrap do 119 | xcode_bootstrap({ 120 | build_configuration_name: 'Beta', 121 | build_configuration_base: :release, 122 | app_suffix: { 123 | 'Debug': { 124 | name: '开发版', 125 | identifier: '.debug' 126 | }, 127 | 'Beta': { 128 | name: '测试版', 129 | identifier: '.beta' 130 | }, 131 | 'Release': { 132 | name: '', 133 | identifier: '' 134 | }, 135 | } 136 | }) 137 | end 138 | ``` 139 | 140 | 打开你的终端执行: 141 | 142 | ```bash 143 | $ fastlane ios bootstrap 144 | ``` 145 | 146 | ## 发布协议 147 | 148 | [MIT License](http://opensource.org/licenses/MIT). 149 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | require 'fastlane' 4 | require 'fastlane-qyer' 5 | require 'awesome_print' 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | 9 | task :default => :spec 10 | 11 | task :test do 12 | 13 | end 14 | 15 | task :helper do 16 | project_path = '/Users/wiiseer/Development/qyer/ios/FastlaneTest/FastlaneTest.xcodeproj' 17 | helper = Fastlane::Qyer::Helper::XcodeHelper.new(project_path) 18 | # 19 | # helper.configurations 20 | # ap helper.project 21 | ap helper.update_build_setting('FastlaneTest', 'debugonly', 'world', 'Debug') 22 | 23 | ap helper.target('FastlaneTest').build_configurations 24 | end -------------------------------------------------------------------------------- /fastlane/AndroidFastfile: -------------------------------------------------------------------------------- 1 | ######################################## 2 | # Android 平台使用 lane 3 | ######################################## 4 | 5 | platform :android do 6 | 7 | 8 | after_all do |_| 9 | UI.message 'All done' 10 | 11 | if is_ci? 12 | UI.header 'Stored context' 13 | Actions.lane_context.each do |key, value| 14 | UI.message "#{key}: #{value}" 15 | end 16 | 17 | # 还原 git 仓库并保留 app/build 文件夹 18 | reset_git_repo(force: true, exclude: ['app/build', 'vendors', 'fastlane']) 19 | end 20 | end 21 | 22 | error do |lane, exception| 23 | handle_errors(lane: lane, exception: exception) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # 设置 fastlane 最低支持版本 2 | fastlane_version '2.125.0' 3 | 4 | ######################################## 5 | # 全局静态变量 6 | ######################################## 7 | 8 | # 关闭匿名统计上报 9 | ENV['FASTLANE_OPT_OUT_USAGE'] = '1'.freeze 10 | 11 | # 关闭自动检查 fastlane 更新 12 | ENV['FASTLANE_SKIP_UPDATE_CHECK'] = '1'.freeze 13 | 14 | ######################################## 15 | # 通用的 lane 任务 16 | ######################################## 17 | 18 | desc 'Fastlane 报错后通用处理' 19 | lane :handle_errors do |options| 20 | lane = options[:lane] 21 | exception = options[:exception] 22 | 23 | clean_build_artifacts 24 | UI.error "Error: #{exception.message}" 25 | UI.error exception.backtrace.join("\n") 26 | 27 | message = exception.message 28 | 29 | if is_ci? 30 | if is_jenkins? 31 | reset_git_repo(force: true) 32 | 33 | message = [ 34 | "【Jenkins - #{ENV['JOB_NAME']}】 构建失败: #{prefix_message}#{message}", 35 | '', 36 | "分支: #{ENV['GIT_BRANCH']}", 37 | "API 环境: #{ENV['APP_API_ENV']}", 38 | "错误阶段: #{lane}", 39 | "构建地址: #{ENV['BUILD_URL']}consoleFull", 40 | '', 41 | '如果能把构建地址的具体错误贴出来会更好的指导研发定位打包错误问题,感谢' 42 | ] 43 | end 44 | 45 | if is_gitlabe_ci? 46 | message = [ 47 | "【Gitlab CI - #{ENV['CI_PROJECT_NAME']}】 构建失败: #{build_error}", 48 | '', 49 | "分支: #{ENV['CI_BUILD_REF_NAME']}", 50 | "API 环境: #{ENV['APP_API_ENV']}", 51 | "错误阶段: #{lane}", 52 | "构建地址: #{ENV['CI_JOB_URL']}", 53 | '', 54 | '如果能把构建地址的具体错误贴出来会更好的指导研发定位打包错误问题,感谢' 55 | ] 56 | end 57 | end 58 | 59 | puts message 60 | end 61 | 62 | desc '提前监测依赖服务是否在线' 63 | lane :precheck_services do |options| 64 | if options[:type] == :ios 65 | service_health( 66 | name: 'Cocoapds CDN 库', 67 | url: 'https://cdn.cocoapods.org/all_pods.txt', 68 | method: :head 69 | ) 70 | end 71 | end 72 | 73 | desc '是否是 Jenkins CI 环境' 74 | lane :is_jenkins? do 75 | ENV.key?('JENKINS_HOME') || ENV.key?('JENKINS_URL') 76 | end 77 | 78 | desc '是否是 Gitlab CI 环境' 79 | lane :is_gitlabe_ci? do 80 | ENV.key?('GITLAB_CI') 81 | end 82 | -------------------------------------------------------------------------------- /fastlane/actions/get_target_identifer.rb: -------------------------------------------------------------------------------- 1 | require 'plist' 2 | require 'xcodeproj' 3 | 4 | module Fastlane 5 | module Actions 6 | class GetTargetIdentiferAction < Action 7 | def self.run(params) 8 | target_filter = params[:target] 9 | project = Xcodeproj::Project.open(params[:xcodeproj]) 10 | targets = project.targets.select { |target| target.name.match(target_filter) } 11 | 12 | UI.user_error! "Not found target: #{target_filter}" if targets.empty? 13 | 14 | target = targets[0] 15 | target.build_configuration_list.build_configurations.each do |build_configuration| 16 | puts build_configuration.name #.to_s.match(/CODE_SIGN_IDENTITY.*/) 17 | end 18 | end 19 | 20 | ##################################################### 21 | # @!group Documentation 22 | ##################################################### 23 | 24 | def self.description 25 | 'Send a success/error message to your [Wechat Work](https://work.weixin.qq.com/) group' 26 | end 27 | 28 | def self.available_options 29 | [ 30 | FastlaneCore::ConfigItem.new(key: :xcodeproj, 31 | env_name: 'UAP_XCODEPROJ', 32 | description: 'The url of webhook', 33 | type: String), 34 | FastlaneCore::ConfigItem.new(key: :target, 35 | env_name: 'UAP_TARGET', 36 | description: 'The value of target', 37 | type: String, 38 | optional: true), 39 | FastlaneCore::ConfigItem.new(key: :export_method, 40 | env_name: 'UAP_EXPORT_METHOD', 41 | description: 'The value of export method', 42 | type: String, 43 | optional: true), 44 | FastlaneCore::ConfigItem.new(key: :identifier, 45 | env_name: 'UAP_INDENTIFIER', 46 | description: 'The value of identifier', 47 | type: String), 48 | FastlaneCore::ConfigItem.new(key: :profile, 49 | env_name: 'UAP_PROFILE', 50 | description: 'The value of profile', 51 | type: String, 52 | optional: true) 53 | ] 54 | end 55 | 56 | def self.category 57 | :misc 58 | end 59 | 60 | def self.authors 61 | ['icyleaf'] 62 | end 63 | 64 | def self.is_supported?(platform) 65 | [:ios, :mac].include?(platform) 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /fastlane/actions/get_xcode_configurations.rb: -------------------------------------------------------------------------------- 1 | require 'plist' 2 | require 'xcodeproj' 3 | 4 | module Fastlane 5 | module Actions 6 | 7 | module SharedValues 8 | XCODE_PROJECT_CONFIGURATIONS = :XCODE_PROJECT_CONFIGURATIONS 9 | end 10 | 11 | 12 | class GetXcodeConfigurationsAction < Action 13 | def self.run(params) 14 | xcodeproj_path = params[:xcodeproj] 15 | UI.user_error! "Not found xcode project: #{xcodeproj_path}" unless Dir.exist?(xcodeproj_path) 16 | 17 | UI.success "Dumping Xcode project's configurations: " 18 | xcodeproj = Xcodeproj::Project.open(xcodeproj_path) 19 | configurations = xcodeproj.build_configurations.map(&:name) 20 | configurations.each do |configuration| 21 | UI.success "- #{configuration}" 22 | end 23 | 24 | Actions.lane_context[SharedValues::XCODE_PROJECT_CONFIGURATIONS] = configurations 25 | end 26 | 27 | ##################################################### 28 | # @!group Documentation 29 | ##################################################### 30 | 31 | def self.description 32 | 'Get Xcode project schemes' 33 | end 34 | 35 | def self.available_options 36 | [ 37 | FastlaneCore::ConfigItem.new(key: :xcodeproj, 38 | env_name: 'GET_XCODE_SCHEMES_XCODEPROJ', 39 | description: 'The path of xcode project', 40 | type: String) 41 | ] 42 | end 43 | 44 | def self.output 45 | [ 46 | ['XCODE_PROJECT_CONFIGURATIONS', 'the configurations of xcode project'] 47 | ] 48 | end 49 | 50 | def self.category 51 | :misc 52 | end 53 | 54 | def self.authors 55 | ['icyleaf'] 56 | end 57 | 58 | def self.is_supported?(platform) 59 | [:ios, :mac].include?(platform) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /fastlane/actions/get_xcode_schemes.rb: -------------------------------------------------------------------------------- 1 | require 'plist' 2 | require 'xcodeproj' 3 | 4 | module Fastlane 5 | module Actions 6 | 7 | module SharedValues 8 | XCODE_PROJECT_SCHEMES = :XCODE_PROJECT_SCHEMES 9 | end 10 | 11 | class GetXcodeSchemesAction < Action 12 | def self.run(params) 13 | xcodeproj_path = params[:xcodeproj] 14 | UI.user_error! "Not found xcode project: #{xcodeproj_path}" unless Dir.exist?(xcodeproj_path) 15 | 16 | UI.success "Dumping Xcode project's schemes: " 17 | schemes = Xcodeproj::Project.schemes(xcodeproj_path) 18 | schemes.each do |scheme| 19 | UI.success "- #{scheme}" 20 | end 21 | 22 | Actions.lane_context[SharedValues::XCODE_PROJECT_SCHEMES] = schemes 23 | end 24 | 25 | ##################################################### 26 | # @!group Documentation 27 | ##################################################### 28 | 29 | def self.description 30 | 'Get Xcode project configurations' 31 | end 32 | 33 | def self.available_options 34 | [ 35 | FastlaneCore::ConfigItem.new(key: :xcodeproj, 36 | env_name: 'GET_XCODE_SCHEMES_XCODEPROJ', 37 | description: 'The path of xcode project', 38 | type: String) 39 | ] 40 | end 41 | 42 | def self.output 43 | [ 44 | ['XCODE_PROJECT_SCHEMES', 'the schemes of xcode project'] 45 | ] 46 | end 47 | 48 | def self.category 49 | :misc 50 | end 51 | 52 | def self.authors 53 | ['icyleaf'] 54 | end 55 | 56 | def self.is_supported?(platform) 57 | [:ios, :mac].include?(platform) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /fastlane/actions/git_last_commit.rb: -------------------------------------------------------------------------------- 1 | require 'fastlane/helper/sh_helper' 2 | require 'date' 3 | 4 | module Fastlane 5 | module Actions 6 | module SharedValues 7 | GIT_LAST_COMMIT_HASH = :GIT_LAST_COMMIT_HASH 8 | GIT_LAST_COMMIT_BRANCH_NAME = :GIT_LAST_COMMIT_BRANCH_NAME 9 | GIT_LAST_COMMIT_COMMITER_NAME = :GIT_LAST_COMMIT_COMMITER_NAME 10 | GIT_LAST_COMMIT_COMMITER_EMAIL = :GIT_LAST_COMMIT_COMMITER_EMAIL 11 | GIT_LAST_COMMIT_DATE = :GIT_LAST_COMMIT_DATE 12 | GIT_LAST_COMMIT_SUBJECT = :GIT_LAST_COMMIT_SUBJECT 13 | end 14 | 15 | ## 16 | # Git Last Commit 17 | class GitLastCommitAction < Action 18 | def self.run(params) 19 | @project_path = params[:path] || File.read_path('.') 20 | UI.user_error!('Not found git repo') unless git_repo? 21 | 22 | dump_to_env 23 | 24 | { 25 | hash: hash, 26 | subject: subject, 27 | branch: branch_name, 28 | commiter_name: commiter_name, 29 | commiter_email: commiter_email, 30 | date: date 31 | } 32 | end 33 | 34 | def self.dump_to_env 35 | output.each do |item| 36 | env_name = item[0] 37 | value = send(env_name.gsub('GIT_LAST_COMMIT_', '').downcase) 38 | Actions.lane_context[SharedValues.const_get(env_name)] = value 39 | end 40 | end 41 | 42 | def self.hash 43 | run_sh('git rev-parse --short HEAD') 44 | end 45 | 46 | def self.date 47 | DateTime.parse(run_sh('git log -1 --format=%ci')) 48 | end 49 | 50 | def self.subject 51 | run_sh('git log -1 --format=%s') 52 | end 53 | 54 | def self.branch_name 55 | run_sh('git rev-parse --abbrev-ref HEAD') 56 | end 57 | 58 | def self.commiter_name 59 | run_sh('git log -1 --format=%cn') 60 | end 61 | 62 | def self.commiter_email 63 | run_sh('git log -1 --format=%ce') 64 | end 65 | 66 | def self.git_repo? 67 | Dir.exist?(File.join(@project_path, '.git')) 68 | end 69 | 70 | def self.run_sh(command) 71 | commands = ["cd #{@project_path}", '&&', command] 72 | Actions.sh(commands.join(' '), log: false).strip 73 | end 74 | 75 | ##################################################### 76 | # @!group Documentation 77 | ##################################################### 78 | 79 | def self.available_options 80 | [ 81 | FastlaneCore::ConfigItem.new(key: :path, 82 | env_name: 'GIT_LAST_COMMIT_PATH', 83 | description: 'The root directory of the git repo. Defaults to `.`', 84 | default_value: '.') 85 | ] 86 | end 87 | 88 | def self.description 89 | 'Get the informations from git last commit' 90 | end 91 | 92 | def self.author 93 | 'icyleaf' 94 | end 95 | 96 | def self.output 97 | [ 98 | ['GIT_LAST_COMMIT_HASH', 'The hash of git last commit'], 99 | ['GIT_LAST_COMMIT_BRANCH_NAME', 'The branch name of git last commit'], 100 | ['GIT_LAST_COMMIT_COMMITER_NAME', 'The commiter name of git last commit'], 101 | ['GIT_LAST_COMMIT_COMMITER_EMAIL', 'The commiter email of git last commit'], 102 | ['GIT_LAST_COMMIT_DATE', 'The date of git last commit'], 103 | ['GIT_LAST_COMMIT_SUBJECT', 'The subject of git last commit'] 104 | ] 105 | end 106 | 107 | def self.is_supported?(platform) 108 | true 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /fastlane/actions/git_remotes.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Actions 3 | class GitRemotesAction < Action 4 | def self.run(_) 5 | `git remote 2>/dev/null`.strip.split("\n") 6 | end 7 | 8 | ##################################################### 9 | # @!group Documentation 10 | ##################################################### 11 | 12 | def self.description 13 | 'Returns the names of the git remote.' 14 | end 15 | 16 | def self.category 17 | :source_control 18 | end 19 | 20 | def self.authors 21 | ['icyleaf'] 22 | end 23 | 24 | def self.return_type 25 | :array 26 | end 27 | 28 | def self.is_supported?(_) 29 | true 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /fastlane/actions/run_time.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Actions 3 | class RunTimeAction < Action 4 | VERSION = '0.1.0'.freeze 5 | 6 | def self.run(params) 7 | filters = params[:filters] 8 | filters = filters.split(',').map(&:chomp) if filters.is_a?(String) 9 | 10 | report = generate_report(console_text, filters) 11 | print_table(report, params[:keep_cost_time]) 12 | end 13 | 14 | def self.print_table(report, keep_cost_time) 15 | rows = report.sort_by { |l| -l[:time] }.each_with_object([]) do |line, obj| 16 | time = line[:time] 17 | next unless (value = keep_cost_time) && time >= value.to_f 18 | 19 | message = line[:message] 20 | obj << ["#{time}s", message] 21 | end 22 | 23 | puts Terminal::Table.new( 24 | title: "Report for run_time #{VERSION}".green, 25 | headings: ['Time', 'Message'], 26 | rows: rows 27 | ) 28 | end 29 | 30 | def self.generate_report(text, filters) 31 | [].tap do |obj| 32 | previous_message = nil 33 | start_time = nil 34 | 35 | text.each_line do |line| 36 | next unless line.start_with?('INFO') 37 | next if filters &.select { |s| line.include?(s) } &.empty? 38 | 39 | line = line.gsub(/\[[\d|;]+m/, '').gsub('▸', '').gsub('', '') 40 | prefix, message = line.split(']: ') 41 | message = message.strip 42 | next if message == '^' 43 | 44 | timestamp_start = prefix.index('[') + 1 45 | timestamp_end = prefix.index(']') 46 | timestamp = Time.parse(prefix[timestamp_start..timestamp_end], '%Y-%m-%d %H:%M:%S.%2N') 47 | 48 | previous_message = message 49 | if start_time.nil? 50 | start_time = timestamp.to_f 51 | next 52 | end 53 | 54 | time = timestamp.to_f - start_time 55 | obj << { 56 | message: previous_message, 57 | started: timestamp, 58 | time: time.round(2) 59 | } 60 | start_time = nil 61 | end 62 | end 63 | end 64 | 65 | def self.console_text 66 | require 'uri' 67 | require 'faraday' 68 | require 'faraday_middleware' 69 | 70 | uri = URI.parse(ENV['BUILD_URL']) 71 | url_path = File.join(uri.path, 'consoleText') 72 | uri.path = '' 73 | 74 | connection = Faraday.new(url: uri.to_s) do |builder| 75 | builder.request(:retry, max: 3, interval: 5) 76 | builder.use(FaradayMiddleware::FollowRedirects) 77 | builder.adapter(:net_http) 78 | end 79 | 80 | begin 81 | connection.get do |req| 82 | req.url(url_path) 83 | end.body.force_encoding('UTF-8') 84 | rescue Faraday::Error::TimeoutError 85 | show_error('Uploading build to Pgyer timed out ⏳', fail_on_error) 86 | end 87 | end 88 | 89 | ##################################################### 90 | # @!group Documentation 91 | ##################################################### 92 | 93 | def self.description 94 | 'Returns the names of the git remote.' 95 | end 96 | 97 | def self.available_options 98 | [ 99 | FastlaneCore::ConfigItem.new(key: :filters, 100 | env_name: 'RUN_TIME_FILTERS', 101 | description: 'The endpoint of apphost', 102 | optional: true, 103 | type: String), 104 | FastlaneCore::ConfigItem.new(key: :keep_cost_time, 105 | env_name: 'RUN_TIME_FILTERS', 106 | description: 'The endpoint of apphost', 107 | optional: true, 108 | default_value: 1, 109 | type: Integer) 110 | ] 111 | end 112 | 113 | def self.category 114 | :misc 115 | end 116 | 117 | def self.authors 118 | ['icyleaf'] 119 | end 120 | 121 | def self.is_supported?(_) 122 | true 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /fastlane/actions/service_health.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Actions 3 | class ServiceHealthAction < Action 4 | VERSION = '0.1.0'.freeze 5 | 6 | def self.run(params) 7 | print_table(params) 8 | 9 | messages = [] 10 | if params[:name] 11 | messages << "[#{params[:name]}]" 12 | else 13 | messages << "[#{URI.parse(params[:url]).host}]" 14 | end 15 | 16 | begin 17 | response = request(params) 18 | handle_reponse(response, params[:accepted_status_codes]) 19 | 20 | code = response.status 21 | # code_message = Net::HTTPResponse::CODE_TO_OBJ[code.to_s] 22 | 23 | messages << "[✅ 服务正常] #{code}" 24 | UI.success(messages.join(' ')) 25 | UI.verbose(response.body) 26 | 27 | true 28 | rescue Faraday::SSLError, Faraday::ConnectionFailed => exception 29 | messages << "[🌐 网络问题] - #{exception.class} - #{exception.message}" 30 | message = messages.join(' ') 31 | UI.crash!(message) 32 | rescue Faraday::ClientError => exception 33 | messages << "[🟡 服务异常] - #{exception.class} - #{exception.message}" 34 | message = messages.join(' ') 35 | UI.crash!(message) 36 | rescue => exception 37 | messages << "[🔴 服务宕机] - #{exception.class} - #{exception.message}" 38 | message = messages.join(' ') 39 | UI.crash!(message) 40 | end 41 | end 42 | 43 | def self.request(params) 44 | connection = build_connection(params) 45 | connection.run_request(params[:method], '', params[:body], params[:headers]) do |req| 46 | req.options.timeout = params[:timeout] if params[:timeout] 47 | end 48 | end 49 | 50 | def self.build_connection(params) 51 | require 'faraday' 52 | require 'faraday_middleware' 53 | 54 | connection = Faraday.new(url: params[:url]) do |builder| 55 | # builder.request(:url_encoded) 56 | # builder.request(:retry, max: 3, interval: 5) 57 | # builder.response(:json, content_type: /\bjson$/) 58 | # builder.use(FaradayMiddleware::FollowRedirects) 59 | 60 | builder.response(:logger, nil, {bodies: true, log_level: :debug}) if params[:enable_request_logger] 61 | builder.adapter(Faraday.default_adapter) 62 | end 63 | end 64 | private_class_method :build_connection 65 | 66 | def self.handle_reponse(response, accepted_status_codes) 67 | codes = flatten_array(accepted_status_codes) 68 | unless codes.include?(response.status) 69 | raise "状态码没有正确匹配: #{response.status}" 70 | end 71 | 72 | true 73 | end 74 | 75 | def self.print_table(form) 76 | rows = form.all_keys.each_with_object({}) do |key, obj| 77 | next if form[key].to_s.empty? 78 | 79 | obj[key] = form[key] 80 | end 81 | 82 | # rows[:client] = "Faraday v#{Faraday::VERSION}" 83 | puts Terminal::Table.new( 84 | title: "Summary for service health #{VERSION}".green, 85 | rows: rows 86 | ) 87 | end 88 | 89 | def self.flatten_array(codes) 90 | codes.each_with_object([]) do |code, obj| 91 | case code 92 | when Array 93 | obj.concat(code) 94 | when Range 95 | obj.concat(code.to_a) 96 | when Integer 97 | obj << code 98 | end 99 | end 100 | end 101 | 102 | ##################################################### 103 | # @!group Documentation 104 | ##################################################### 105 | 106 | def self.description 107 | 'Service health check' 108 | end 109 | 110 | # sh "curl --form plat_id=#{plat_id} --form file_nick_name=#{app_name} --form token=a83c617952d0a6129616d0d307ab840e841935d7 --form file=@#{file} https://app.haohaozhu.me/api/pkgs" 111 | def self.available_options 112 | [ 113 | FastlaneCore::ConfigItem.new(key: :url, 114 | env_name: 'SERVICE_HEALTH_URL', 115 | description: 'The url of service', 116 | type: String), 117 | FastlaneCore::ConfigItem.new(key: :name, 118 | env_name: 'SERVICE_HEALTH_URL', 119 | description: 'The name of service', 120 | type: String, 121 | optional: true), 122 | FastlaneCore::ConfigItem.new(key: :timeout, 123 | env_name: 'SERVICE_HEALTH_TIMEOUT', 124 | description: 'The timeout of request url', 125 | default_value: 30, 126 | type: Integer, 127 | optional: true), 128 | FastlaneCore::ConfigItem.new(key: :method, 129 | env_name: 'SERVICE_HEALTH_METHOD', 130 | description: 'The method of url', 131 | default_value: :get, 132 | type: Symbol, 133 | optional: true, 134 | verify_block: proc do |value| 135 | UI.user_error!("method must be #{Faraday::Connection::METHODS.to_a.join(', ')}") unless Faraday::Connection::METHODS.include?(value) 136 | end 137 | ), 138 | FastlaneCore::ConfigItem.new(key: :accepted_status_codes, 139 | env_name: 'SERVICE_HEALTH_ACCEPTED_STATUS_CODES', 140 | description: 'Accepted status codes of url response', 141 | default_value: [200..299], 142 | type: Array, 143 | optional: true), 144 | FastlaneCore::ConfigItem.new(key: :headers, 145 | env_name: 'SERVICE_HEALTH_HEADERS', 146 | description: 'The headers of request url', 147 | type: Hash, 148 | optional: true), 149 | FastlaneCore::ConfigItem.new(key: :body, 150 | env_name: 'SERVICE_HEALTH_BODY', 151 | description: 'The body of request url', 152 | type: String, 153 | optional: true), 154 | FastlaneCore::ConfigItem.new(key: :enable_request_logger, 155 | env_name: 'SERVICE_HEALTH_ENABLE_REAUEST_LOGGER', 156 | description: 'Enable request logger by using Faraday (true/false)', 157 | optional: true, 158 | default_value: false, 159 | type: Boolean) 160 | ] 161 | end 162 | 163 | def self.example_code 164 | [ 165 | 'service_health( 166 | url: "...", 167 | method: :get, 168 | query: "", 169 | headers: {}, 170 | body: {}, 171 | retries: 0, 172 | timeout: 10, 173 | accept_status_codes: [200, 201, 202, 203, 204, 205, 206, 207, 208, 226], 174 | )' 175 | ] 176 | end 177 | 178 | def self.category 179 | :misc 180 | end 181 | 182 | def self.authors 183 | ['icyleaf'] 184 | end 185 | 186 | def self.is_supported?(_) 187 | true 188 | end 189 | end 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /fastlane/actions/update_entitlements.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Actions 3 | class UpdateEntitlementsAction < Action 4 | require 'plist' 5 | 6 | def self.run(params) 7 | key = params[:key] 8 | value = params[:value] 9 | 10 | UI.message("Entitlements File: #{params[:entitlements_file]}") 11 | UI.message("Update key: #{key} to #{value}") 12 | 13 | entitlements_file = params[:entitlements_file] 14 | UI.user_error!("Could not find entitlements file at path '#{entitlements_file}'") unless File.exist?(entitlements_file) 15 | 16 | # parse entitlements 17 | result = Plist.parse_xml(entitlements_file) 18 | UI.user_error!("Entitlements file at '#{entitlements_file}' cannot be parsed.") unless result 19 | 20 | old_value = result[key] 21 | # UI.user_error!("No existing key #{key}. Please make sure it in the entitlements file.") unless old_value && value.nil?a 22 | 23 | UI.message("Old value: #{old_value}") 24 | if value.empty? 25 | result.delete(key) 26 | UI.message("Old value is removed") 27 | else 28 | result[key] = params[:identifiers] 29 | UI.message("New value: #{result[key]}") 30 | end 31 | 32 | result.save_plist(entitlements_file) 33 | UI.message("New value: #{result[key]}") 34 | end 35 | 36 | ##################################################### 37 | # @!group Documentation 38 | ##################################################### 39 | 40 | def self.description 41 | 'This action changes the value of given key in the entitlements file' 42 | end 43 | 44 | def self.available_options 45 | [ 46 | FastlaneCore::ConfigItem.new(key: :entitlements_file, 47 | env_name: "FL_UPDATE_ENTITLEMENTS_FILE_PATH", # The name of the environment variable 48 | description: "The path to the entitlement file which contains the keychain access groups", # a short description of this parameter 49 | verify_block: proc do |value| 50 | UI.user_error!("Please pass a path to an entitlements file. ") unless value.include?(".entitlements") 51 | UI.user_error!("Could not find entitlements file") if !File.exist?(value) && !Helper.test? 52 | end), 53 | FastlaneCore::ConfigItem.new(key: :key, 54 | env_name: "FL_UPDATE_ENTITLEMENTS_KEY", 55 | description: "An key of entitlements. Eg. 'your.keychain.access.groups.identifiers'", 56 | is_string: true), 57 | FastlaneCore::ConfigItem.new(key: :value, 58 | env_name: "FL_UPDATE_ENTITLEMENTS_VALUE", 59 | description: "An value of entitlements, to remove set it nil") 60 | ] 61 | end 62 | 63 | def self.category 64 | :project 65 | end 66 | 67 | def self.authors 68 | ['icyleaf'] 69 | end 70 | 71 | def self.is_supported?(platform) 72 | platform == :ios 73 | end 74 | 75 | def self.example_code 76 | [ 77 | 'update_entitlements( 78 | entitlements_file: "/path/to/entitlements_file.entitlements", 79 | key: "your.keychain.access.groups.identifiers", 80 | value: "value" 81 | )' 82 | ] 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /fastlane/actions/update_target_provisioning.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Actions 3 | class UpdateTargetProvisioningAction < Action 4 | def self.run(params) 5 | require 'plist' 6 | require 'xcodeproj' 7 | 8 | project = Xcodeproj::Project.open(params[:xcodeproj]) 9 | end 10 | 11 | # # 手动设置 Profile 12 | # identifiers = [ 13 | # { 14 | # target: 'HaoHaoZhu', 15 | # identifier: 'com.haohaozhu.hhz' 16 | # }, 17 | # { 18 | # target: 'NotificationService', 19 | # identifier: 'com.haohaozhu.hhz.NotificationService' 20 | # } 21 | # ] 22 | 23 | # identifiers.each do |item| 24 | # env_key = "sigh_#{item[:identifier]}_#{export_method}_profile-path" 25 | # profile_file = ENV[env_key] 26 | 27 | # update_project_provisioning( 28 | # xcodeproj: 'HaoHaoZhu.xcodeproj', 29 | # profile: profile_file, 30 | # target_filter: item[:target] 31 | # ) 32 | # end 33 | 34 | ##################################################### 35 | # @!group Documentation 36 | ##################################################### 37 | 38 | def self.description 39 | 'Send a success/error message to your [Wechat Work](https://work.weixin.qq.com/) group' 40 | end 41 | 42 | def self.available_options 43 | [ 44 | FastlaneCore::ConfigItem.new(key: :xcodeproj, 45 | env_name: 'UAP_XCODEPROJ', 46 | description: 'The url of webhook', 47 | type: String), 48 | FastlaneCore::ConfigItem.new(key: :target, 49 | env_name: 'UAP_TARGET', 50 | description: 'The type of message', 51 | type: String, 52 | optional: true), 53 | FastlaneCore::ConfigItem.new(key: :export_method, 54 | env_name: 'UAP_TARGET', 55 | description: 'The type of message', 56 | type: String, 57 | optional: true), 58 | FastlaneCore::ConfigItem.new(key: :identifier, 59 | env_name: 'UAP_INDENTIFIER', 60 | description: 'The content of message', 61 | type: String), 62 | FastlaneCore::ConfigItem.new(key: :profile, 63 | env_name: 'UAP_PROFILE', 64 | description: 'The path of profile', 65 | type: String, 66 | optional: true) 67 | ] 68 | end 69 | 70 | def self.category 71 | :misc 72 | end 73 | 74 | def self.authors 75 | ['icyleaf'] 76 | end 77 | 78 | def self.is_supported?(platform) 79 | [:ios, :mac].include?(platform) 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /fastlane/actions/update_user_defined.rb: -------------------------------------------------------------------------------- 1 | require 'xcodeproj' 2 | require 'plist' 3 | 4 | module Fastlane 5 | module Actions 6 | 7 | # WIP 8 | class UpdateUserDefinedAction < Action 9 | def self.run(params) 10 | @project_path = params[:project_path] 11 | UI.user_error!('Could not find Xcode project') unless File.exist?(@project_path) 12 | UI.user_error!('Please pass the name of user-defined setting') unless params[:name] 13 | UI.user_error!('Please pass the value of user-defined setting') unless params[:value] 14 | 15 | @xcode_hepler = Fastlane::Qyer::Helper::XcodeHelper.new(@project_path) 16 | 17 | @xcode_hepler.update_build_setting(params[:target], params[:name], params[:value], params[:configuration]) 18 | true 19 | end 20 | 21 | def self.available_options 22 | [ 23 | FastlaneCore::ConfigItem.new(key: :project_path, 24 | env_name: 'QYER_UPDATE_USER_DEFINES_PROJECT_PATH', 25 | description: 'Project (.xcodeproj) file to use to build app', 26 | default_value: Dir['*.xcodeproj'].first, 27 | optional: true), 28 | FastlaneCore::ConfigItem.new(key: :target, 29 | env_name: 'QYER_UPDATE_USER_DEFINES_TARGET', 30 | description: 'The target name of the project', 31 | default_value: 0, 32 | optional: true), 33 | FastlaneCore::ConfigItem.new(key: :name, 34 | env_name: 'QYER_UPDATE_USER_DEFINES_NAME', 35 | description: 'The name of variable'), 36 | FastlaneCore::ConfigItem.new(key: :value, 37 | env_name: 'QYER_UPDATE_USER_DEFINES_VALUE', 38 | description: 'The value of variable'), 39 | FastlaneCore::ConfigItem.new(key: :configuration, 40 | env_name: 'QYER_UPDATE_USER_DEFINES_CONFIGURATION', 41 | description: 'The name of build configuration (Set all if leave it)', 42 | optional: true), 43 | FastlaneCore::ConfigItem.new(key: :overwrite, 44 | env_name: 'QYER_UPDATE_USER_DEFINES_OVERWRITE', 45 | description: 'Overwrite if the variable is exist', 46 | default_value: false, 47 | is_string: false) 48 | ] 49 | end 50 | 51 | def self.output 52 | [] 53 | end 54 | 55 | def self.description 56 | 'Custom user-defined variable for xcode project' 57 | end 58 | 59 | def self.details 60 | 'Custom user-defined variable for xcode project' 61 | end 62 | 63 | def self.author 64 | 'icyleaf' 65 | end 66 | 67 | def self.is_supported?(platform) 68 | [:ios, :mac].include? platform 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /fastlane/actions/wechat_work.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Actions 3 | class WechatWorkAction < Action 4 | def self.run(params) 5 | webhook_url = params[:webhook_url] 6 | type = params[:type].to_sym 7 | message = params[:message] 8 | to = params[:to] 9 | fail_on_error = params[:fail_on_error] 10 | 11 | bot = WeChatWorkGroupBot.new(webhook_url) 12 | response = bot.send(type, message, to) 13 | if response 14 | parse_response(response, fail_on_error) 15 | else 16 | message = "Error pushing Wechat Work message: #{bot.exception.message}" 17 | if fail_on_error 18 | UI.user_error!(message) 19 | else 20 | UI.error(message) 21 | end 22 | end 23 | end 24 | 25 | def self.parse_response(response, fail_on_error) 26 | body = JSON.parse(response.body) 27 | 28 | if body.key?('errcode') && body['errcode'].zero? 29 | # {"errcode":0,"errmsg":"ok"} 30 | UI.success('Successfully sent Wechat Work notification') 31 | else 32 | UI.verbose(body) 33 | # {"errcode"=>93000, "errmsg"=>"invalid webhook url, hint: [1562216438_5_dc6f8cbc429c382cac201e37cdb30e3d]"} 34 | # {"errcode"=>93000, "errmsg"=>"Invalid input : content exceed max length. invalid Request Parameter, hint: [1564552858_1_d7176fe91aaf622920a73574287222a5]"} 35 | message = "Error pushing Wechat Work message: [#{body['errcode']}] #{body['errmsg']}" 36 | if fail_on_error 37 | UI.user_error!(message) 38 | else 39 | UI.error(message) 40 | end 41 | end 42 | end 43 | 44 | ##################################################### 45 | # @!group Documentation 46 | ##################################################### 47 | 48 | def self.description 49 | 'Send a success/error message to your [Wechat Work](https://work.weixin.qq.com/) group' 50 | end 51 | 52 | def self.available_options 53 | [ 54 | FastlaneCore::ConfigItem.new(key: :webhook_url, 55 | env_name: 'WECHATWORK_WEBHOOK_URL', 56 | description: 'The url of webhook', 57 | type: String), 58 | FastlaneCore::ConfigItem.new(key: :type, 59 | env_name: 'WECHATWORK_TYPE', 60 | description: 'The type of message', 61 | type: Symbol, 62 | default_value: :text, 63 | optional: true), 64 | FastlaneCore::ConfigItem.new(key: :message, 65 | env_name: 'WECHATWORK_MESSAGEE', 66 | description: 'The content of message', 67 | type: String), 68 | FastlaneCore::ConfigItem.new(key: :to, 69 | env_name: 'WECHATWORK_TO', 70 | description: 'The users of group (only use :text type of message)', 71 | type: Array, 72 | optional: true), 73 | FastlaneCore::ConfigItem.new(key: :fail_on_error, 74 | env_name: 'WECHATWORK_FAIL_ON_ERROR', 75 | description: 'Should an error sending the Wechat Work notification cause a failure? (true/false)', 76 | optional: true, 77 | default_value: false, 78 | type: Boolean) 79 | ] 80 | end 81 | 82 | def self.category 83 | :notifications 84 | end 85 | 86 | def self.example_code 87 | [ 88 | 'wechat_work( 89 | webhook_url: "...", 90 | message: "hello world" 91 | )', 92 | 93 | 'wechat_work( 94 | webhook_url: "...", 95 | type: :markdown 96 | message: "# Google it\n\n [link](https://google.com)" 97 | )', 98 | 99 | 'wechat_work( 100 | webhook_url: "...", 101 | message: "Reply me", 102 | to: ["13800138000", "@all"] 103 | )' 104 | ] 105 | end 106 | 107 | def self.authors 108 | ['icyleaf'] 109 | end 110 | 111 | def self.is_supported?(_) 112 | true 113 | end 114 | end 115 | end 116 | end 117 | 118 | class WeChatWorkGroupBot 119 | require 'net/http' 120 | require 'digest' 121 | require 'base64' 122 | require 'json' 123 | 124 | MESSAGE_TYPES = [:text, :markdown, :image, :news].freeze 125 | IMAGE_TYPES = ['jpg', 'jpeg', 'png'].freeze 126 | 127 | attr_reader :exception 128 | 129 | @exception = nil 130 | 131 | def initialize(webhook_url) 132 | @webhook_url = URI(webhook_url) 133 | end 134 | 135 | def send_message(text, to = nil) 136 | send(:text, text, to) 137 | end 138 | 139 | def send_markdown(text, to = nil) 140 | send(:markdown, text, to) 141 | end 142 | 143 | def send_image(image_file, to = nil) 144 | # send(:image, image_file, to) 145 | end 146 | 147 | def send_news(title, description, image_url, link) 148 | end 149 | 150 | def send(type, text, to = nil) 151 | determine_type!(type) 152 | 153 | @exception = nil 154 | data = build_body(type, text, to) 155 | Net::HTTP.post(@webhook_url, data.to_json, 'Content-Type' => 'application/json') 156 | rescue => e 157 | @exception = e 158 | 159 | nil 160 | end 161 | 162 | private 163 | 164 | def build_body(type, message, to) 165 | data = {} 166 | data[:msgtype] = type 167 | if type == :image 168 | # NOTE: 图片(base64编码前)最大不能超过2M,支持JPG,PNG格式 169 | 170 | file = message 171 | determine_image!(file) 172 | data[type] = { 173 | base64: image_base64_encode(file), 174 | md5: Digest::MD5.file(file) 175 | } 176 | else 177 | data[type] = { content: message } 178 | end 179 | 180 | data[type].merge!(handle_mention(to)) 181 | data 182 | end 183 | 184 | def handle_mention(to) 185 | list = { 186 | mentioned_list: [], 187 | mentioned_mobile_list: [], 188 | } 189 | 190 | tos = [] 191 | case to 192 | when String 193 | tos << to 194 | when Array 195 | tos.concat(to) 196 | end 197 | 198 | tos.each do |value| 199 | if mobile_phone_number?(value) 200 | list[:mentioned_mobile_list] << value 201 | else 202 | list[:mentioned_list] << value 203 | end 204 | end 205 | 206 | list 207 | end 208 | 209 | def mobile_phone_number?(value) 210 | value.match(/\d{11}/) 211 | end 212 | 213 | def image_base64_encode(path) 214 | File.open(path, 'rb') do |file| 215 | Base64.strict_encode64(file.read) 216 | end 217 | end 218 | 219 | def determine_type!(type) 220 | raise "Not match type: #{type}, avaiable in : #{MESSAGE_TYPES}" unless MESSAGE_TYPES.include?(type) 221 | end 222 | 223 | def determine_image!(file) 224 | raise 'Not a image file' unless File.file?(file) 225 | raise 'Only avaiable in JPG, PNG image' unless IMAGE_TYPES.include?(File.extname(file)) 226 | end 227 | end 228 | -------------------------------------------------------------------------------- /fastlane/actions/xcode_bootstrap.rb: -------------------------------------------------------------------------------- 1 | require 'xcodeproj' 2 | require 'plist' 3 | 4 | module Fastlane 5 | module Actions 6 | module SharedValues 7 | XCODE_PROJECT_PATH = :XCODE_PROJECT_PATH 8 | XCODE_PROJECT_NAME = :XCODE_PROJECT_NAME 9 | XCODE_APP_IDENTIFIER = :XCODE_APP_IDENTIFIER 10 | end 11 | 12 | # WIP 13 | class XcodeBootstrapAction < Action 14 | def self.run(params) 15 | @project_path = params[:project_path] 16 | 17 | UI.user_error!('Please pass the path to the project (.xcodeproj)') unless @project_path.to_s.end_with?('.xcodeproj') 18 | UI.user_error!('Could not find Xcode project') unless File.exist?(@project_path) 19 | 20 | @project_name = @project_path.split('/')[-1] 21 | @use_cocoapods = params[:cocoapods] 22 | @app_suffix = params[:app_suffix] 23 | @app_identifier = ENV['PRODUCE_APP_IDENTIFIER'] 24 | 25 | app_suffix_valid! 26 | 27 | @build_configuration_name = params[:build_configuration_name] 28 | @build_configuration_base = params[:build_configuration_base] 29 | 30 | @project = Xcodeproj::Project.open(@project_path) 31 | UI.user_error!('Not found any target in project') if @project.targets.empty? 32 | 33 | @target_name = @project.targets[0].name 34 | if @use_cocoapods 35 | if pods_exists? 36 | podfile_bootstrap! 37 | else 38 | UI.user_error('Podfile does not exist.') 39 | end 40 | end 41 | 42 | xcode_bootstrap! 43 | 44 | # return as success 45 | UI.success('bootstrap up! 🚀') 46 | end 47 | 48 | def self.xcode_bootstrap! 49 | if @project.build_configuration_list[@build_configuration_name] 50 | UI.user_error!("Build configuration `#{@build_configuration_name}` is exists, check again.") 51 | else 52 | add_build_configuration(@build_configuration_name, @build_configuration_base) 53 | @project.save 54 | end 55 | end 56 | 57 | def self.podfile_bootstrap! 58 | add_build_configuration_to_podfile = "xcodeproj '#{project_name.split('.')[0]}', '#{build_configuration_name}' => :#{build_configuration_base}" 59 | 60 | command = %(cat #{podfile_path} | grep "#{add_build_configuration_to_podfile}" | wc -l) 61 | r = Actions.sh(command, log: false) 62 | unless r.strip.to_i > 0 63 | UI.message("Adding #{build_configuration_name} build configuration to Podfile") 64 | tempfile = "#{podfile_path}.tmp" 65 | File.open(tempfile, 'w') do |f| 66 | File.foreach(podfile_path) do |line| 67 | match_line = /target(\s+)["|']#{target_name}["|'](\s+)do/ 68 | f.puts line 69 | if StringScanner.new(line).match?(match_line) 70 | f.puts "\t#{add_build_configuration_to_podfile}" 71 | end 72 | end 73 | end 74 | 75 | File.delete(podfile_path) 76 | File.rename(tempfile, podfile_path) 77 | end 78 | end 79 | 80 | def self.pods_exists? 81 | File.exist?(File.join(@project_path, 'Podfile')) 82 | end 83 | 84 | def self.add_build_configuration(name, base) 85 | @project.add_build_configuration(name, base) 86 | 87 | project_target = @project.targets[0] 88 | code_sign = (base == :release) ? 'iPhone Distribution' : 'iPhone Developer' 89 | 90 | target_configuration = project_target.add_build_configuration(name, base) 91 | target_configuration.base_configuration_reference = pod_build_configuration(@build_configuration_name) 92 | target_configuration.build_settings['SDKROOT'] = 'iphoneos' 93 | target_configuration.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = @app_identifier 94 | target_configuration.build_settings['INFOPLIST_FILE'] = info_plist_path 95 | target_configuration.build_settings['CODE_SIGN_IDENTITY[sdk=iphoneos*]'] = code_sign 96 | target_configuration.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = ["#{name.upcase}=1"] 97 | 98 | target_configuration 99 | end 100 | 101 | def self.pod_build_configuration(name) 102 | @project.objects.select do |obj| 103 | obj.isa == 'PBXFileReference' && 104 | !obj.name.nil? && 105 | obj.name.include?(".#{name.downcase}.xcconfig") 106 | end[0] 107 | end 108 | 109 | def self.info_plist_path 110 | @project.objects.select do |obj| 111 | obj.isa == 'XCBuildConfiguration' && 112 | !obj.build_settings['PRODUCT_BUNDLE_IDENTIFIER'].nil? 113 | end[0].build_settings['INFOPLIST_FILE'] 114 | end 115 | 116 | def self.app_suffix_valid! 117 | UI.user_error!('Invaild app suffix format. please check `fastlane action xcode_bootstrap`') unless @app_suffix.class == Hash 118 | @app_suffix.each do |_name, dict| 119 | UI.user_error!('no') unless dict.class == Hash 120 | UI.user_error!('no') unless dict.keys == [:name, :identifier] 121 | dict.each do |_key, value| 122 | UI.user_error!('no') unless value.class == String 123 | end 124 | end 125 | end 126 | 127 | def self.available_options 128 | [ 129 | FastlaneCore::ConfigItem.new(key: :project_path, 130 | env_name: 'XCODE_PROJECT_PATH', 131 | description: 'Project (.xcodeproj) file to use to build app', 132 | default_value: Dir['*.xcodeproj'].first, 133 | optional: true), 134 | FastlaneCore::ConfigItem.new(key: :cocoapods, 135 | env_name: 'XCODE_COCOAPODS_SUPPORT', 136 | description: 'Project need cocoapods support(default is `false`)', 137 | is_string: false, 138 | default_value: false), 139 | FastlaneCore::ConfigItem.new(key: :build_configuration_name, 140 | env_name: 'XCODE_BUILD_CONFIGURATION_NAME', 141 | description: 'The build configuration name of your app', 142 | default_value: 'AdHoc'), 143 | FastlaneCore::ConfigItem.new(key: :build_configuration_base, 144 | env_name: 'XCODE_BUILD_CONFIGURATION_BASE', 145 | description: 'The build configuration base name of your app', 146 | is_string: false, 147 | default_value: :release), 148 | FastlaneCore::ConfigItem.new(key: :app_suffix, 149 | env_name: 'XCODE_APP_SUFFIX', 150 | description: 'The user defined app name&identifier suffix of your app', 151 | is_string: false, 152 | default_value: { 153 | 'Debug' => { 154 | name: '开发版', 155 | identifier: '.debug' 156 | }, 157 | 'AdHoc' => { 158 | name: '内测版', 159 | identifier: '' 160 | }, 161 | 'Release' => { 162 | name: '', 163 | identifier: '' 164 | } 165 | }) 166 | ] 167 | end 168 | 169 | def self.output 170 | [ 171 | ['XCODE_PROJECT_PATH', 'The xcode project path of your app'], 172 | ['XCODE_PROJECT_NAME', 'The xcode project name of your app'], 173 | ['XCODE_APP_IDENTIFIER', 'The identifier of your appp'] 174 | ] 175 | end 176 | 177 | def self.description 178 | 'Easy bootstrap xcode project (alpine)' 179 | end 180 | 181 | def self.details 182 | 'Quick append build configuration and support cocoapods' 183 | end 184 | 185 | def self.author 186 | 'icyleaf' 187 | end 188 | 189 | def self.is_supported?(platform) 190 | [:ios, :mac].include? platform 191 | end 192 | end 193 | end 194 | end 195 | -------------------------------------------------------------------------------- /fastlane/helper/xcode.rb: -------------------------------------------------------------------------------- 1 | require 'xcodeproj' 2 | require 'plist' 3 | 4 | module Fastlane 5 | module Qyer 6 | module Helper 7 | class XcodeHelper 8 | def initialize(project_path) 9 | @project_path = project_path || Dir['*.xcodeproj'].first 10 | end 11 | 12 | def target(name) 13 | if name.is_a?(Integer) 14 | project.targets[name] 15 | else 16 | targets = project.targets.select { |t| t.name == name } 17 | targets.empty? ? nil : targets[0] 18 | end 19 | end 20 | 21 | def info 22 | @info ||= Plist.parse_xml(info_plist_path) 23 | end 24 | 25 | def update_build_setting(target_name, key, value, config_name = nil) 26 | if config_name.to_s.empty? 27 | update_all_build_setting(target_name, key, value) 28 | else 29 | update_build_setting_by_name(target_name, config_name, key, value) 30 | end 31 | end 32 | 33 | def update_all_build_setting(target_name, key, value) 34 | target(target_name).build_configurations.each do |configuration| 35 | configuration.build_settings[key] = value 36 | end 37 | project.save 38 | end 39 | 40 | def update_build_setting_by_name(target_name, config_name, key, value) 41 | if has_configuration?(target(target_name), config_name) 42 | target(target_name).build_configurations.each do |configuration| 43 | configuration.build_settings[key] = value if configuration.name == config_name 44 | end 45 | project.save 46 | end 47 | end 48 | 49 | def has_configuration?(target, name) 50 | !target.build_configurations.select {|c| c.name == name }.empty? 51 | end 52 | 53 | def info_plist_path 54 | unless @info_plist_path 55 | path = project.objects.select { |obj| obj.isa == 'XCBuildConfiguration' && !obj.build_settings['PRODUCT_BUNDLE_IDENTIFIER'].nil? }[0].build_settings['INFOPLIST_FILE'] 56 | @info_plist_path = File.join(File.path(@project_path), '..', path) 57 | end 58 | 59 | @info_plist_path 60 | end 61 | 62 | def project 63 | @project ||= Xcodeproj::Project.open(@project_path) 64 | end 65 | end 66 | end 67 | end 68 | end -------------------------------------------------------------------------------- /fastlane/iOSFastfile: -------------------------------------------------------------------------------- 1 | # 设置 xcode 构建获取配置的超时 30 分钟 2 | ENV['FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT'] = '30' 3 | 4 | platform :ios do 5 | desc '注册测试设备' 6 | desc '注册时必须提供设备名和 UDID' 7 | desc "fastlane device name:'name' udid:'udid'" 8 | lane :device do |options| 9 | name = options[:name] 10 | udid = options[:udid] 11 | 12 | UI.user_error!('*NAME* is missing.') if name.to_s.empty? 13 | UI.user_error!('*UDID* is missing.') if udid.to_s.empty? 14 | 15 | register_devices(devices: { name => udid }) 16 | end 17 | 18 | after_all do 19 | UI.message 'All done' 20 | if is_ci? 21 | UI.header 'Stored context' 22 | Actions.lane_context.each do |key, value| 23 | UI.message "#{key}: #{value}" 24 | end 25 | 26 | # 还原 git 仓库并保留 Pods 和 build 文件夹 27 | reset_git_repo(force: true, exclude: %w[build Pods vendors fastlane]) 28 | 29 | remove_ram_disk if Actions.lane_context[Actions::SharedValues::RAM_DISK_PATH] 30 | end 31 | end 32 | 33 | error do |lane, exception| 34 | remove_ram_disk if is_ci? && Actions.lane_context[Actions::SharedValues::RAM_DISK_PATH] 35 | handle_errors(lane: lane, exception: exception) 36 | end 37 | 38 | end 39 | --------------------------------------------------------------------------------