Skip to content

Commit cbe1552

Browse files
authored
Add support for release branch freezing and unfreezing (#56)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1201899738287924/task/1211100722861854?focus=true This change adds a new freeze_release_branch_action that is called by the “promote” workflow on Fridays. The branch is then unfrozen by tag_release_action when run for a public release (if a branch wasn’t frozen, it handles it gracefully - this will be the case for hotfixes). Internal release bump workflows run validate_internal_release_bump_action that now starts with a check for a frozen branch, and if that fails, the workflow is stopped.
1 parent e857a8f commit cbe1552

13 files changed

Lines changed: 592 additions & 131 deletions

lib/fastlane/plugin/ddg_apple_automation/actions/asana_find_release_task_action.rb

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def self.run(params)
3838
setup_constants(platform)
3939

4040
UI.message("Checking latest marketing version")
41-
latest_marketing_version = find_latest_marketing_version(github_token, params[:platform])
41+
latest_marketing_version = Helper::GitHelper.find_latest_marketing_version(github_token, params[:platform])
4242
UI.success("Latest marketing version: #{latest_marketing_version}")
4343
UI.message("Searching for release task for version #{latest_marketing_version}")
4444
release_task_id, hotfix_task_id = find_release_task(latest_marketing_version, asana_access_token)
@@ -64,31 +64,6 @@ def self.run(params)
6464
}
6565
end
6666

67-
def self.find_latest_marketing_version(github_token, platform)
68-
latest_internal_release = Helper::GitHelper.latest_release(Helper::GitHelper.repo_name, true, platform, github_token)
69-
70-
version = extract_version_from_tag_name(latest_internal_release&.tag_name)
71-
if version.to_s.empty?
72-
Fastlane::UI.user_error!("Failed to find latest marketing version")
73-
return
74-
end
75-
unless self.validate_semver(version)
76-
Fastlane::UI.user_error!("Invalid marketing version: #{version}, expected format: MAJOR.MINOR.PATCH")
77-
return
78-
end
79-
version
80-
end
81-
82-
def self.extract_version_from_tag_name(tag_name)
83-
# Remove build number (if present) and platform suffix from the tag name
84-
tag_name&.split(/[-+]/)&.first
85-
end
86-
87-
def self.validate_semver(version)
88-
# we only need basic "x.y.z" validation here
89-
version.match?(/\A\d+\.\d+\.\d+\z/)
90-
end
91-
9267
def self.find_release_task(version, asana_access_token)
9368
asana_client = Helper::AsanaHelper.make_asana_client(asana_access_token)
9469
release_task_id = nil

lib/fastlane/plugin/ddg_apple_automation/actions/asana_report_failed_workflow_action.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ def self.run(params)
1818

1919
extra_collaborators = []
2020

21-
if params[:commit_sha]
22-
args[:last_commit_url] = "https://github.com/#{Helper::GitHelper.repo_name}/commit/#{params[:commit_sha]}"
23-
commit_author = Helper::GitHelper.commit_author(Helper::GitHelper.repo_name, params[:commit_sha], params[:github_token])
21+
commit_sha = params[:commit_sha].to_s.strip
22+
23+
unless commit_sha.empty?
24+
args[:last_commit_url] = "https://github.com/#{Helper::GitHelper.repo_name}/commit/#{commit_sha}"
25+
commit_author = Helper::GitHelper.commit_author(Helper::GitHelper.repo_name, commit_sha, params[:github_token])
2426
args[:last_commit_author_id] = Helper::AsanaHelper.get_asana_user_id_for_github_handle(commit_author)
2527
extra_collaborators << args[:last_commit_author_id]
2628
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
require "fastlane/action"
2+
require "fastlane_core/configuration/config_item"
3+
require "octokit"
4+
require_relative "../helper/ddg_apple_automation_helper"
5+
require_relative "../helper/git_helper"
6+
7+
module Fastlane
8+
module Actions
9+
class FreezeReleaseBranchAction < Action
10+
def self.run(params)
11+
platform = params[:platform] || Actions.lane_context[Actions::SharedValues::PLATFORM_NAME]
12+
13+
begin
14+
Helper::GitHelper.freeze_release_branch(platform, params[:github_token], other_action)
15+
rescue StandardError => e
16+
UI.important("Failed to create GitHub release")
17+
Helper::DdgAppleAutomationHelper.report_error(e)
18+
end
19+
end
20+
21+
def self.description
22+
"Adds a draft public release for the latest marketing version as an indicator of a frozen release branch"
23+
end
24+
25+
def self.authors
26+
["DuckDuckGo"]
27+
end
28+
29+
def self.return_value
30+
""
31+
end
32+
33+
def self.details
34+
"This action checks the latest marketing version in GitHub and creates a draft public release for it.
35+
If the release already exists, it does nothing."
36+
end
37+
38+
def self.available_options
39+
[
40+
FastlaneCore::ConfigItem.github_token,
41+
FastlaneCore::ConfigItem.platform
42+
]
43+
end
44+
45+
def self.is_supported?(platform)
46+
true
47+
end
48+
end
49+
end
50+
end

lib/fastlane/plugin/ddg_apple_automation/actions/tag_release_action.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require_relative "asana_extract_task_id_action"
88
require_relative "asana_log_message_action"
99
require_relative "asana_create_action_item_action"
10+
require_relative "asana_report_failed_workflow_action"
1011

1112
module Fastlane
1213
module Actions
@@ -22,12 +23,36 @@ def self.setup_constants(platform)
2223
end
2324
end
2425

26+
# rubocop:disable Metrics/PerceivedComplexity
2527
def self.run(params)
2628
platform = params[:platform] || Actions.lane_context[Actions::SharedValues::PLATFORM_NAME]
2729
Helper::GitHelper.setup_git_user
2830

2931
setup_constants(platform)
3032

33+
unless params[:is_prerelease]
34+
branch = other_action.git_branch
35+
begin
36+
Helper::GitHelper.unfreeze_release_branch(branch, platform, params[:github_token])
37+
rescue StandardError => e
38+
Helper::GitHubActionsHelper.set_output("stop_workflow", true)
39+
Helper::DdgAppleAutomationHelper.report_error(e)
40+
UI.important("Failed to unfreeze release branch. Cannot proceed with the public release. Please unfreeze manually and run the workflow again.")
41+
task_id = AsanaExtractTaskIdAction.run(task_url: params[:asana_task_url])
42+
AsanaReportFailedWorkflowAction.run(
43+
asana_access_token: params[:asana_access_token],
44+
github_token: params[:github_token],
45+
platform: platform,
46+
task_id: task_id,
47+
branch: branch,
48+
github_handle: params[:github_handle],
49+
workflow_name: "Tag Release",
50+
workflow_url: ENV.fetch("WORKFLOW_URL", nil)
51+
)
52+
return
53+
end
54+
end
55+
3156
unless assert_branch_tagged_before_public_release(params.values)
3257
UI.important("Skipping release because release branch's HEAD is not tagged.")
3358
Helper::GitHubActionsHelper.set_output("stop_workflow", true)
@@ -85,6 +110,7 @@ def self.assert_branch_tagged_before_public_release(params)
85110
end
86111
true
87112
end
113+
# rubocop:enable Metrics/PerceivedComplexity
88114

89115
def self.create_tag_and_github_release(is_prerelease, platform, github_token)
90116
tag, promoted_tag = Helper::DdgAppleAutomationHelper.compute_tag(is_prerelease, platform)

lib/fastlane/plugin/ddg_apple_automation/actions/validate_internal_release_bump_action.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ def self.run(params)
1414
options = params.values
1515
find_release_task_if_needed(options)
1616

17+
Helper::GitHelper.assert_release_branch_is_not_frozen(options[:release_branch], params[:platform], options[:github_token])
18+
1719
if params[:is_scheduled_release] && !Helper::GitHelper.assert_branch_has_changes(options[:release_branch], params[:platform])
1820
UI.important("No changes to the release branch (or only changes to scripts and workflows). Skipping automatic release.")
1921
Helper::GitHubActionsHelper.set_output("skip_release", true)

lib/fastlane/plugin/ddg_apple_automation/helper/git_helper.rb

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,38 @@ def self.commit_sha_for_tag(tag)
9090
`git rev-parse "#{tag}"^{}`.chomp
9191
end
9292

93-
def self.latest_release(repo_name, prerelease, platform, github_token)
93+
def self.find_latest_marketing_version(github_token, platform)
94+
latest_internal_release = Helper::GitHelper.latest_release(Helper::GitHelper.repo_name, true, platform, github_token)
95+
96+
version = extract_version_from_tag_name(latest_internal_release&.tag_name)
97+
if version.to_s.empty?
98+
UI.user_error!("Failed to find latest marketing version")
99+
return
100+
end
101+
unless self.validate_semver(version)
102+
UI.user_error!("Invalid marketing version: #{version}, expected format: MAJOR.MINOR.PATCH")
103+
return
104+
end
105+
version
106+
end
107+
108+
def self.extract_version_from_tag_name(tag_name)
109+
# Remove build number (if present) and platform suffix from the tag name
110+
tag_name&.split(/[-+]/)&.first
111+
end
112+
113+
def self.extract_version_from_branch_name(branch_name)
114+
version = branch_name.split('/')&.last
115+
version if validate_semver(version)
116+
end
117+
118+
def self.validate_semver(version)
119+
# we only need basic "x.y.z" validation here
120+
version.match?(/\A\d+\.\d+\.\d+\z/)
121+
end
122+
123+
# rubocop:disable Metrics/PerceivedComplexity
124+
def self.latest_release(repo_name, prerelease, platform, github_token, allow_drafts: false)
94125
client = Octokit::Client.new(access_token: github_token)
95126

96127
current_page = 1
@@ -104,6 +135,9 @@ def self.latest_release(repo_name, prerelease, platform, github_token)
104135
# If `prerelease` is false, then ensure that the release is public.
105136
matching_release = releases.find do |release|
106137
matches_platform = platform.nil? || release.tag_name.end_with?("+#{platform}")
138+
if allow_drafts
139+
matches_platform ||= release.name.end_with?("+#{platform}")
140+
end
107141
matches_prerelease = prerelease == release.prerelease
108142
matches_platform && matches_prerelease
109143
end
@@ -116,6 +150,107 @@ def self.latest_release(repo_name, prerelease, platform, github_token)
116150

117151
return nil
118152
end
153+
# rubocop:enable Metrics/PerceivedComplexity
154+
155+
def self.delete_release(release_url, github_token)
156+
client = Octokit::Client.new(access_token: github_token)
157+
client.delete_release(release_url)
158+
end
159+
160+
def self.freeze_release_branch(platform, github_token, other_action)
161+
UI.message("Checking latest marketing version")
162+
latest_marketing_version = find_latest_marketing_version(github_token, platform)
163+
UI.success("Latest marketing version: #{latest_marketing_version}")
164+
165+
draft_public_release_name = "#{latest_marketing_version}+#{platform}"
166+
167+
UI.message("Will freeze release branch for #{latest_marketing_version} by creating a draft public release")
168+
UI.message("First we'll check if #{draft_public_release_name} release exists.")
169+
170+
UI.message("Checking for draft public release #{draft_public_release_name}")
171+
latest_public_release = latest_release(Helper::GitHelper.repo_name, false, platform, github_token, allow_drafts: true)
172+
UI.success("Latest public release (including drafts): #{latest_public_release.name}")
173+
174+
if latest_public_release.name == draft_public_release_name
175+
UI.success("Draft public release #{draft_public_release_name} already exists. Nothing to do as the branch is already frozen.")
176+
return
177+
end
178+
179+
UI.message("Creating draft public release #{draft_public_release_name}")
180+
181+
description = <<~DESCRIPTION
182+
This draft release is here to indicate that the release branch is frozen.
183+
New internal releases on `release/#{platform}/#{latest_marketing_version}` branch cannot be created.
184+
If you need to bump the internal release, please manually delete this draft release.
185+
DESCRIPTION
186+
187+
other_action.set_github_release(
188+
repository_name: repo_name,
189+
api_bearer: github_token,
190+
description: description,
191+
name: draft_public_release_name,
192+
tag_name: "",
193+
is_draft: true,
194+
is_prerelease: false
195+
)
196+
UI.success("Draft public release #{draft_public_release_name} created")
197+
end
198+
199+
def self.assert_release_branch_is_not_frozen(release_branch, platform, github_token)
200+
UI.message("Checking if release on #{release_branch} branch can be bumped.")
201+
202+
marketing_version = extract_version_from_branch_name(release_branch)
203+
if marketing_version.to_s.empty?
204+
UI.user_error!("Unable to extract version from '#{release_branch}' branch name.")
205+
return
206+
end
207+
208+
UI.message("Version extracted from '#{release_branch}' branch name: #{marketing_version}")
209+
210+
draft_public_release_name = "#{marketing_version}+#{platform}"
211+
UI.message("Checking if draft public release #{draft_public_release_name} exists.")
212+
213+
latest_public_release = latest_release(repo_name, false, platform, github_token, allow_drafts: true)
214+
UI.success("Latest public release (including drafts): #{latest_public_release.name}")
215+
216+
if latest_public_release.name == draft_public_release_name && latest_public_release.draft
217+
UI.important("Draft public release #{draft_public_release_name} exists, which means the release branch is frozen.")
218+
UI.error("🚨 If you need to bump the release:")
219+
UI.error(" - Delete the draft public release to unfreeze the branch")
220+
UI.error(" - Release URL: ➡️ #{latest_public_release.html_url} ⬅️")
221+
UI.error(" - Restart the workflow")
222+
UI.user_error!("Release branch is frozen.")
223+
return
224+
end
225+
226+
UI.success("No draft public release #{draft_public_release_name} found - the release isn't frozen.")
227+
end
228+
229+
def self.unfreeze_release_branch(release_branch, platform, github_token)
230+
marketing_version = extract_version_from_branch_name(release_branch)
231+
if marketing_version.to_s.empty?
232+
UI.user_error!("Unable to extract version from '#{release_branch}' branch name.")
233+
return
234+
end
235+
236+
UI.message("Unfreezing release branch #{release_branch} if needed")
237+
238+
draft_public_release_name = "#{marketing_version}+#{platform}"
239+
UI.message("Checking if draft public release #{draft_public_release_name} exists.")
240+
241+
latest_public_release = latest_release(repo_name, false, platform, github_token, allow_drafts: true)
242+
UI.success("Latest public release (including drafts): #{latest_public_release.name}")
243+
244+
unless latest_public_release.name == draft_public_release_name && latest_public_release.draft
245+
UI.important("Latest public release is not a draft. No need to delete it.")
246+
return
247+
end
248+
249+
UI.message("Release version matches and it's a draft release.")
250+
UI.important("Deleting draft public release #{draft_public_release_name}")
251+
delete_release(latest_public_release.url, github_token)
252+
UI.success("Draft public release #{draft_public_release_name} deleted")
253+
end
119254

120255
def self.commit_author(repo_name, commit_sha, github_token)
121256
client = Octokit::Client.new(access_token: github_token)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module Fastlane
22
module DdgAppleAutomation
3-
VERSION = "2.11.0"
3+
VERSION = "3.0.0"
44
end
55
end

0 commit comments

Comments
 (0)