|
| 1 | +module Fastlane |
| 2 | + module Actions |
| 3 | + class PrototypeBuildDetailsCommentAction < Action |
| 4 | + def self.run(params) |
| 5 | + app_display_name = params[:app_display_name] |
| 6 | + app_center_info = AppCenterInfo.from_params(params) |
| 7 | + metadata = consolidate_metadata(params, app_center_info) |
| 8 | + |
| 9 | + qr_code_url, extra_metadata = build_install_links(app_center_info, params[:download_url]) |
| 10 | + metadata.merge!(extra_metadata) |
| 11 | + |
| 12 | + # Build the comment parts |
| 13 | + icon_img_tag = img_tag(params[:app_icon] || app_center_info.icon, alt: app_display_name) |
| 14 | + metadata_rows = metadata.compact.map { |key, value| "<tr><td><b>#{key}</b></td><td>#{value}</td></tr>" } |
| 15 | + intro = "#{icon_img_tag}📲 You can test the changes from this Pull Request in <b>#{app_display_name}</b> by scanning the QR code below to install the corresponding build." |
| 16 | + footnote = params[:footnote] || (app_center_info.org_name.nil? ? '' : DEFAULT_APP_CENTER_FOOTNOTE) |
| 17 | + body = <<~COMMENT_BODY |
| 18 | + <table> |
| 19 | + <tr> |
| 20 | + <td rowspan='#{metadata_rows.count + 1}' width='260px'><img src='#{qr_code_url}' width='250' height='250' /></td> |
| 21 | + <td><b>App Name</b></td><td>#{icon_img_tag} #{app_display_name}</td> |
| 22 | + </tr> |
| 23 | + #{metadata_rows.join("\n")} |
| 24 | + </table> |
| 25 | + #{footnote} |
| 26 | + COMMENT_BODY |
| 27 | + |
| 28 | + if params[:fold] |
| 29 | + "<details><summary>#{intro}</summary>\n#{body}</details>\n" |
| 30 | + else |
| 31 | + "<p>#{intro}</p>\n#{body}" |
| 32 | + end |
| 33 | + end |
| 34 | + |
| 35 | + ##################################################### |
| 36 | + # @!group Helpers |
| 37 | + ##################################################### |
| 38 | + |
| 39 | + NO_INSTALL_URL_ERROR_MESSAGE = <<~NO_URL_ERROR.freeze |
| 40 | + No URL provided to download or install the app. |
| 41 | + - Either use this action right after using `appcenter_upload` and provide an `app_center_org_name` (so that this action can use the link to the App Center build) |
| 42 | + - Or provide an explicit value for the `download_url` parameter |
| 43 | + NO_URL_ERROR |
| 44 | + |
| 45 | + DEFAULT_APP_CENTER_FOOTNOTE = '<em>Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.</em>'.freeze |
| 46 | + |
| 47 | + # A small model struct to consolidate and pack all the values related to App Center |
| 48 | + # |
| 49 | + AppCenterInfo = Struct.new(:org_name, :app_name, :display_name, :release_id, :icon, :version, :short_version, :os, :bundle_id) do |
| 50 | + # A method to construct an AppCenterInfo instance from the action params, and infer the rest from the `lane_context` if available |
| 51 | + def self.from_params(params) |
| 52 | + org_name = params[:app_center_org_name] |
| 53 | + ctx = if org_name && defined?(SharedValues::APPCENTER_BUILD_INFORMATION) |
| 54 | + Fastlane::Actions.lane_context[SharedValues::APPCENTER_BUILD_INFORMATION] || {} |
| 55 | + else |
| 56 | + {} |
| 57 | + end |
| 58 | + app_name = params[:app_center_app_name] || ctx['app_name'] |
| 59 | + new( |
| 60 | + org_name, |
| 61 | + app_name, |
| 62 | + ctx['app_display_name'] || app_name, |
| 63 | + params[:app_center_release_id] || ctx['id'], |
| 64 | + ctx['app_icon_url'], |
| 65 | + ctx['version'], |
| 66 | + ctx['short_version'], |
| 67 | + ctx['app_os'], |
| 68 | + ctx['bundle_identifier'] |
| 69 | + ) |
| 70 | + end |
| 71 | + end |
| 72 | + |
| 73 | + # Builds the installation link, QR code URL and extra metadata for download links from the available info |
| 74 | + # |
| 75 | + # @param [AppCenterInfo] app_center_info The struct containing all the values related to App Center info |
| 76 | + # @param [String] download_url The `download_url` parameter passed to the action, if one exists |
| 77 | + # @return [(String, Hash<String,String>)] A tuple containing: |
| 78 | + # - The URL for the QR Code |
| 79 | + # - A Hash of the extra metadata key/value pairs to add to the existing metadata, to enrich them with download/install links |
| 80 | + # |
| 81 | + def self.build_install_links(app_center_info, download_url) |
| 82 | + install_url = nil |
| 83 | + extra_metadata = {} |
| 84 | + if download_url |
| 85 | + install_url = download_url |
| 86 | + extra_metadata['Direct Download'] = "<a href='#{install_url}'><code>#{File.basename(install_url)}</code></a>" |
| 87 | + end |
| 88 | + if app_center_info.org_name && app_center_info.app_name |
| 89 | + install_url = "https://install.appcenter.ms/orgs/#{app_center_info.org_name}/apps/#{app_center_info.app_name}/releases/#{app_center_info.release_id}" |
| 90 | + extra_metadata['App Center Build'] = "<a href='#{install_url}'>#{app_center_info.display_name} \##{app_center_info.release_id}</a>" |
| 91 | + end |
| 92 | + UI.user_error!(NO_INSTALL_URL_ERROR_MESSAGE) if install_url.nil? |
| 93 | + qr_code_url = "https://chart.googleapis.com/chart?chs=500x500&cht=qr&chl=#{CGI.escape(install_url)}&choe=UTF-8" |
| 94 | + [qr_code_url, extra_metadata] |
| 95 | + end |
| 96 | + |
| 97 | + # A method to build the Hash of metadata, based on the explicit ones passed by the user as parameter + the implicit ones from `AppCenterInfo` |
| 98 | + # |
| 99 | + # @param [Hash<Symbol, Any>] params The action's parameters, as received by `self.run` |
| 100 | + # @param [AppCenterInfo] app_center_info The model object containing all the values related to App Center information |
| 101 | + # @return [Hash<String, String>] A hash of all the metadata, gathered from both the explicit and the implicit ones |
| 102 | + # |
| 103 | + def self.consolidate_metadata(params, app_center_info) |
| 104 | + metadata = params[:metadata]&.transform_keys(&:to_s) || {} |
| 105 | + metadata['Build Number'] ||= app_center_info.version |
| 106 | + metadata['Version'] ||= app_center_info.short_version |
| 107 | + metadata[app_center_info.os == 'Android' ? 'Application ID' : 'Bundle ID'] ||= app_center_info.bundle_id |
| 108 | + # (Feel free to add more CI-specific env vars in the line below to support other CI providers if you need) |
| 109 | + metadata['Commit'] ||= ENV.fetch('BUILDKITE_COMMIT', nil) || other_action.last_git_commit[:abbreviated_commit_hash] |
| 110 | + metadata |
| 111 | + end |
| 112 | + |
| 113 | + # Creates an HTML `<img>` tag for an icon URL or the image URL to represent a given Buildkite emoji |
| 114 | + # |
| 115 | + # @param [String] url_or_emoji A `String` which can be: |
| 116 | + # - Either a valid URI to an image |
| 117 | + # - Or a string formatted like `:emojiname:`, using a valid Buildite emoji name as defined in https://github.com/buildkite/emojis |
| 118 | + # @param [String] alt The alt text to use for the `<img>` tag |
| 119 | + # @return [String] The `<img …>` tag with the proper image and alt tag |
| 120 | + # |
| 121 | + def self.img_tag(url_or_emoji, alt: '') |
| 122 | + return nil if url_or_emoji.nil? |
| 123 | + |
| 124 | + emoji = url_or_emoji.match(/:(.*):/)&.captures&.first |
| 125 | + app_icon_url = if emoji |
| 126 | + "https://raw.githubusercontent.com/buildkite/emojis/main/img-buildkite-64/#{emoji}.png" |
| 127 | + elsif URI(url_or_emoji) |
| 128 | + url_or_emoji |
| 129 | + end |
| 130 | + app_icon_url ? "<img alt='#{alt}' align='top' src='#{app_icon_url}' width='20px' />" : '' |
| 131 | + end |
| 132 | + |
| 133 | + ##################################################### |
| 134 | + # @!group Documentation |
| 135 | + ##################################################### |
| 136 | + |
| 137 | + def self.description |
| 138 | + 'Generates a string providing all the details of a prototype build, nicely-formatted and ready to be used as a PR comment (e.g. via `comment_on_pr`).' |
| 139 | + end |
| 140 | + |
| 141 | + def self.details |
| 142 | + <<~DESC |
| 143 | + Generates a string providing all the details of a prototype build, nicely-formatted as HTML. |
| 144 | + The returned string will typically be subsequently used by the `comment_on_pr` action to post that HTML as comment on a PR. |
| 145 | +
|
| 146 | + If you used the `appcenter_upload` lane (to upload the Prototype build to App Center) before calling this action, and pass |
| 147 | + a value to the `app_center_org_name` parameter, then many of the parameters and metadata will be automatically extracted |
| 148 | + from the `lane_context` provided by `appcenter_upload`, including: |
| 149 | +
|
| 150 | + - The `app_center_app_name`, `app_center_release_id` and installation URL to use for the QR code to point to that release in App Center |
| 151 | + - The `app_icon` |
| 152 | + - The app's Build Number / versionCode |
| 153 | + - The app's Version / versionName |
| 154 | + - The app's Bundle ID / Application ID |
| 155 | + - A `footnote` mentioning the MC tool for Automatticians to add themselves to App Center |
| 156 | +
|
| 157 | + This means that if you are using App Center to distribute your Prototype Build, the only parameters you *have* to provide |
| 158 | + to this action are `app_display_name` and `app_center_org_name`; plus, for `metadata` most of the interesting values will already be pre-filled. |
| 159 | +
|
| 160 | + Any of those implicit default values/metadata can of course be overridden by passing an explicit value to the appropriate parameter(s). |
| 161 | + DESC |
| 162 | + end |
| 163 | + |
| 164 | + def self.available_options |
| 165 | + app_center_auto = '(will be automatically extracted from `lane_context if you used `appcenter_upload` to distribute your Prototype build)' |
| 166 | + [ |
| 167 | + FastlaneCore::ConfigItem.new( |
| 168 | + key: :app_display_name, |
| 169 | + env_name: 'FL_PROTOTYPE_BUILD_DETAILS_COMMENT_APP_DISPLAY_NAME', |
| 170 | + description: 'The display name to use for the app in the comment message', |
| 171 | + optional: false, |
| 172 | + type: String |
| 173 | + ), |
| 174 | + FastlaneCore::ConfigItem.new( |
| 175 | + key: :app_center_org_name, |
| 176 | + env_name: 'APPCENTER_OWNER_NAME', # Intentionally the same as the one used by the `appcenter_upload` action |
| 177 | + description: 'The name of the organization in App Center (if you used `appcenter_upload` to distribute your Prototype build)', |
| 178 | + type: String, |
| 179 | + optional: true |
| 180 | + ), |
| 181 | + FastlaneCore::ConfigItem.new( |
| 182 | + key: :app_center_app_name, |
| 183 | + env_name: 'APPCENTER_APP_NAME', # Intentionally the same as the one used by the `appcenter_upload` action |
| 184 | + description: "The name of the app in App Center #{app_center_auto}", |
| 185 | + type: String, |
| 186 | + optional: true, |
| 187 | + default_value_dynamic: true # As it will be extracted from the `lane_context`` if you used `appcenter_upload`` |
| 188 | + ), |
| 189 | + FastlaneCore::ConfigItem.new( |
| 190 | + key: :app_center_release_id, |
| 191 | + env_name: 'APPCENTER_RELEASE_ID', |
| 192 | + description: "The release ID/Number in App Center #{app_center_auto}", |
| 193 | + type: String, |
| 194 | + optional: true, |
| 195 | + default_value_dynamic: true # As it will be extracted from the `lane_context`` if you used `appcenter_upload`` |
| 196 | + ), |
| 197 | + FastlaneCore::ConfigItem.new( |
| 198 | + key: :app_icon, |
| 199 | + env_name: 'FL_PROTOTYPE_BUILD_DETAILS_COMMENT_APP_ICON', |
| 200 | + description: "The name of an emoji from the https://github.com/buildkite/emojis list or the full image URL to use for the icon of the app in the message. #{app_center_auto}", |
| 201 | + type: String, |
| 202 | + optional: true, |
| 203 | + default_value_dynamic: true # As it will be extracted from the `lane_context`` if you used `appcenter_upload`` |
| 204 | + ), |
| 205 | + FastlaneCore::ConfigItem.new( |
| 206 | + key: :download_url, |
| 207 | + env_name: 'FL_PROTOTYPE_BUILD_DETAILS_COMMENT_DOWNLOAD_URL', |
| 208 | + description: 'The URL to download the build as a direct download. ' \ |
| 209 | + + 'If you uploaded the build to App Center, we recommend leaving this nil (the comment will use the URL to the App Center build for the QR code)', |
| 210 | + type: String, |
| 211 | + optional: true, |
| 212 | + default_value: nil |
| 213 | + ), |
| 214 | + FastlaneCore::ConfigItem.new( |
| 215 | + key: :fold, |
| 216 | + env_name: 'FL_PROTOTYPE_BUILD_DETAILS_COMMENT_FOLD', |
| 217 | + description: 'If true, will wrap the HTML table inside a <details> block (hidden by default)', |
| 218 | + type: Boolean, |
| 219 | + default_value: false |
| 220 | + ), |
| 221 | + FastlaneCore::ConfigItem.new( |
| 222 | + key: :metadata, |
| 223 | + env_name: 'FL_PROTOTYPE_BUILD_DETAILS_COMMENT_METADATA', |
| 224 | + description: 'All additional metadata (as key/value pairs) you want to include in the HTML table of the comment. ' \ |
| 225 | + + 'If you are running this action after `appcenter_upload`, some metadata will automatically be added to this list too', |
| 226 | + type: Hash, |
| 227 | + optional: true, |
| 228 | + default_value_dynamic: true # As some metadata will be auto-filled if you used `appcenter_upload` |
| 229 | + ), |
| 230 | + FastlaneCore::ConfigItem.new( |
| 231 | + key: :footnote, |
| 232 | + env_name: 'FL_PROTOTYPE_BUILD_DETAILS_COMMENT_FOOTNOTE', |
| 233 | + description: 'Optional footnote to add below the HTML table of the comment. ' \ |
| 234 | + + 'If you are running this action after `appcenter_upload`, a default footnote for Automatticians will be used unless you provide an explicit value', |
| 235 | + type: String, |
| 236 | + optional: true, |
| 237 | + default_value_dynamic: true # We have a default footnote for the case when you used App Center |
| 238 | + ), |
| 239 | + ] |
| 240 | + end |
| 241 | + |
| 242 | + def self.return_type |
| 243 | + :string |
| 244 | + end |
| 245 | + |
| 246 | + def self.return_value |
| 247 | + 'The HTML comment containing all the relevant info about a Prototype build and links to install it' |
| 248 | + end |
| 249 | + |
| 250 | + def self.authors |
| 251 | + ['Automattic'] |
| 252 | + end |
| 253 | + |
| 254 | + def self.is_supported?(platform) |
| 255 | + true |
| 256 | + end |
| 257 | + end |
| 258 | + end |
| 259 | +end |
0 commit comments