Skip to content

Commit 8fa96d8

Browse files
authored
Merge pull request #449 from wordpress-mobile/action/prototype-build-comment
Add `prototype_build_details_comment` action
2 parents aea6166 + f1f93ca commit 8fa96d8

File tree

3 files changed

+834
-0
lines changed

3 files changed

+834
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
- Add new `buildkite_annotate` action to add/remove annotations from the current build. [#442]
1414
- Add new `buildkite_metadata` action to set/get metadata from the current build. [#442]
15+
- Add new `prototype_build_details_comment` action to make it easier to generate the HTML comment about Prototype Builds in PRs. [#449]
1516

1617
### Bug Fixes
1718

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
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

Comments
 (0)