-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
353 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
298 changes: 298 additions & 0 deletions
298
lib/fastlane/plugin/ddg_apple_automation/actions/tag_release_action.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,298 @@ | ||
require "fastlane/action" | ||
require "fastlane_core/configuration/config_item" | ||
require "octokit" | ||
require_relative "../helper/ddg_apple_automation_helper" | ||
require_relative "../helper/git_helper" | ||
require_relative "asana_extract_task_id_action" | ||
require_relative "asana_log_message_action" | ||
require_relative "asana_create_action_item_action" | ||
|
||
module Fastlane | ||
module Actions | ||
class TagReleaseAction < Action | ||
@constants = {} | ||
|
||
def self.setup_constants(platform) | ||
case platform | ||
when "ios" | ||
@constants = { | ||
repo_name: "duckduckgo/ios" | ||
} | ||
when "macos" | ||
@constants = { | ||
repo_name: "duckduckgo/macos-browser" | ||
} | ||
end | ||
end | ||
|
||
def self.run(params) | ||
platform = params[:platform] || Actions.lane_context[Actions::SharedValues::PLATFORM_NAME] | ||
setup_constants(platform) | ||
|
||
base_branch = params[:base_branch] | ||
branch = params[:branch] | ||
dmg_url_prefix = params[:dmg_url_prefix] | ||
github_elevated_permissions_token = params[:github_elevated_permissions_token] | ||
github_token = params[:github_token] | ||
is_prerelease = params[:is_prerelease] | ||
|
||
other_action.ensure_git_branch(branch: "^(:?release|hotfix)/.*$") | ||
Helper::GitHelper.setup_git_user | ||
|
||
tag, promoted_tag, tag_created, latest_public_release_tag = create_tag_and_github_release(is_prerelease, github_token) | ||
|
||
Helper::GitHubActionsHelper.set_output("tag", tag) | ||
|
||
merge_or_delete_failed = false | ||
begin | ||
if is_prerelease | ||
merge_branch(branch, base_branch, github_elevated_permissions_token) | ||
else | ||
delete_branch(branch, github_token) | ||
end | ||
rescue StandardError | ||
merge_or_delete_failed = true | ||
end | ||
|
||
# Set environment variables | ||
|
||
ENV['TAG'] = tag | ||
ENV['PROMOTED_TAG'] = promoted_tag | ||
dmg_version = (is_prerelease ? tag : promoted_tag).gsub('-', '.') | ||
ENV['DMG_URL'] = "#{dmg_url_prefix}duckduckgo-#{dmg_version}.dmg" | ||
ENV['RELEASE_URL'] = "https://github.com/#{@constants[:repo_name]}/releases/tag/#{tag}" | ||
unless tag_created | ||
ENV['LAST_RELEASE_TAG'] = latest_public_release_tag | ||
end | ||
|
||
report_status(params, merge_or_delete_failed) | ||
end | ||
|
||
def self.report_status(params, merge_or_delete_failed) | ||
task_template, comment_template = set_up_asana_templates(params, merge_or_delete_failed) | ||
|
||
if task_template | ||
AsanaCreateActionItemAction.run( | ||
asana_access_token: params[:asana_access_token], | ||
task_url: params[:asana_task_url], | ||
template_name: task_template, | ||
github_handle: params[:github_handle], | ||
is_scheduled_release: params[:is_scheduled_release] | ||
) | ||
|
||
if params[:is_internal_release_bump] | ||
AsanaCreateActionItemAction.run( | ||
asana_access_token: params[:asana_access_token], | ||
task_url: params[:asana_task_url], | ||
template_name: "run-publish-dmg-release", | ||
github_handle: params[:github_handle], | ||
is_scheduled_release: params[:is_scheduled_release] | ||
) | ||
end | ||
end | ||
|
||
AsanaLogMessageAction.run( | ||
asana_access_token: params[:asana_access_token], | ||
task_url: params[:asana_task_url], | ||
template_name: comment_template, | ||
is_scheduled_release: params[:is_scheduled_release] | ||
) | ||
end | ||
|
||
def self.set_up_asana_templates(params, merge_or_delete_failed) | ||
if merge_or_delete_failed | ||
case [tag_created, is_prerelease] | ||
when [true, true] | ||
task_template = "merge-failed" | ||
comment_template = "internal-release-ready-merge-failed" | ||
when [true, false] | ||
task_template = "delete-branch-failed" | ||
comment_template = "public-release-tagged-delete-branch-failed" | ||
when [false, true] | ||
task_template = "internal-release-tag-failed" | ||
comment_template = "internal-release-ready-tag-failed" | ||
when [false, false] | ||
task_template = "public-release-tag-failed" | ||
comment_template = "public-release-tag-failed" | ||
end | ||
else | ||
comment_template = is_prerelease ? "internal-release-ready" : "public-release-tagged" | ||
end | ||
|
||
return task_template, comment_template | ||
end | ||
|
||
def self.create_tag_and_github_release(is_prerelease, github_token) | ||
tag, promoted_tag = compute_tag(is_prerelease) | ||
tag_created = false | ||
|
||
begin | ||
other_action.add_git_tag(tag: tag) | ||
other_action.push_git_tags(tag: tag) | ||
tag_created = true | ||
rescue StandardError => e | ||
UI.warn("Failed to create and push tag: #{e}") | ||
return tag, promoted_tag, tag_created, nil | ||
end | ||
|
||
begin | ||
client = Octokit::Client.new(access_token: github_token) | ||
latest_public_release = client.latest_release(@constants[:repo_name]) | ||
|
||
# Octokit doesn't provide the API to generate release notes for a specific tag | ||
# So we need to use the GitHub API directly | ||
generate_release_notes = other_action.github_api( | ||
api_token: github_token, | ||
http_method: "POST", | ||
path: "/repos/#{@constants[:repo_name]}/releases/generate-notes", | ||
body: { | ||
tag_name: tag, | ||
previous_tag_name: latest_public_release.tag_name | ||
} | ||
) | ||
|
||
release_notes = JSON.parse(generate_release_notes[:body]) | ||
|
||
other_action.set_github_release( | ||
repository_name: @constants[:repo_name], | ||
api_token: github_token, | ||
tag_name: tag, | ||
name: release_notes&.dig('name'), | ||
description: release_notes&.dig('body'), | ||
is_prerelease: is_prerelease | ||
) | ||
rescue StandardError => e | ||
UI.warn("Failed to create GitHub release: #{e}") | ||
end | ||
|
||
return tag, promoted_tag, tag_created, latest_public_release.tag_name | ||
end | ||
|
||
def self.compute_tag(is_prerelease) | ||
version = File.read("Configuration/Version.xcconfig").chomp.split(" = ").last | ||
build_number = File.read("Configuration/BuildNumber.xcconfig").chomp.split(" = ").last | ||
if is_prerelease | ||
tag = "#{version}-#{build_number}" | ||
else | ||
tag = version | ||
promoted_tag = "#{version}-#{build_number}" | ||
end | ||
|
||
return tag, promoted_tag | ||
end | ||
|
||
def self.merge_branch(branch, base_branch, github_token) | ||
client = Octokit::Client.new(access_token: github_token) | ||
begin | ||
client.merge(@constants[:repo_name], base_branch, branch) | ||
rescue StandardError => e | ||
UI.warn("Failed to merge #{branch} branch to #{base_branch}: #{e}") | ||
raise e | ||
end | ||
end | ||
|
||
def self.delete_branch(branch, github_token) | ||
client = Octokit::Client.new(access_token: github_token) | ||
begin | ||
client.delete_branch(@constants[:repo_name], branch) | ||
rescue StandardError => e | ||
UI.warn("Failed to delete #{branch}: #{e}") | ||
raise e | ||
end | ||
end | ||
|
||
def self.validate_params(task_id, task_url, comment, template_name, workflow_url) | ||
if task_id.to_s.empty? && task_url.to_s.empty? | ||
raise ArgumentError, "Both task_id and task_url cannot be empty. At least one must be provided." | ||
end | ||
|
||
if comment.to_s.empty? && template_name.to_s.empty? | ||
raise ArgumentError, "Both comment and template_name cannot be empty. At least one must be provided." | ||
end | ||
|
||
if comment && workflow_url.to_s.empty? | ||
raise ArgumentError, "If comment is provided, workflow_url cannot be empty" | ||
end | ||
end | ||
|
||
def self.description | ||
"Tags the release in GitHub and merges release branch to main" | ||
end | ||
|
||
def self.authors | ||
["DuckDuckGo"] | ||
end | ||
|
||
def self.return_value | ||
"" | ||
end | ||
|
||
def self.details | ||
# Optional: | ||
"" | ||
end | ||
|
||
def self.available_options | ||
[ | ||
FastlaneCore::ConfigItem.asana_access_token, | ||
FastlaneCore::ConfigItem.github_token, | ||
FastlaneCore::ConfigItem.new(key: :asana_task_url, | ||
description: "Asana release task URL", | ||
optional: false, | ||
type: String), | ||
FastlaneCore::ConfigItem.new(key: :base_branch, | ||
description: "Base branch name (defaults to main, only override for testing)", | ||
optional: true, | ||
type: String, | ||
default_value: "main"), | ||
FastlaneCore::ConfigItem.new(key: :branch, | ||
description: "Release branch name", | ||
optional: false, | ||
type: String), | ||
FastlaneCore::ConfigItem.new(key: :dmg_url_prefix, | ||
description: "Constant prefix of the DMG URL", | ||
optional: false, | ||
type: String), | ||
FastlaneCore::ConfigItem.new(key: :github_handle, | ||
description: "Github user handle", | ||
optional: true, | ||
type: String), | ||
FastlaneCore::ConfigItem.new(key: :github_elevated_permissions_token, | ||
env_name: "GITHUB_ELEVATED_PERMISSIONS_TOKEN", | ||
description: "GitHub token with elevated permissions (allowing to bypass branch protections)", | ||
optional: false, | ||
sensitive: true, | ||
type: String, | ||
verify_block: proc do |value| | ||
UI.user_error!("GITHUB_ELEVATED_PERMISSIONS_TOKEN is not set") if value.to_s.length == 0 | ||
end), | ||
FastlaneCore::ConfigItem.new(key: :is_internal_release_bump, | ||
description: "Is this an internal release bump? (the subsequent internal release of the current week)", | ||
optional: true, | ||
type: Boolean, | ||
default_value: false), | ||
FastlaneCore::ConfigItem.new(key: :is_prerelease, | ||
description: "Is this a pre-release? (a.k.a. internal release)", | ||
optional: false, | ||
type: Boolean), | ||
FastlaneCore::ConfigItem.new(key: :is_scheduled_release, | ||
description: "Indicates whether the release was scheduled or started manually", | ||
optional: true, | ||
type: Boolean, | ||
default_value: false), | ||
FastlaneCore::ConfigItem.new(key: :platform, | ||
description: "Platform (iOS or macOS) - optionally to override lane context value", | ||
optional: true, | ||
type: String, | ||
verify_block: proc do |value| | ||
UI.user_error!("platform must be equal to 'ios' or 'macos'") unless ['ios', 'macos'].include?(value.to_s) | ||
end) | ||
] | ||
end | ||
|
||
def self.is_supported?(platform) | ||
true | ||
end | ||
end | ||
end | ||
end |
31 changes: 31 additions & 0 deletions
31
lib/fastlane/plugin/ddg_apple_automation/helper/git_helper.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
require "fastlane/action" | ||
require "fastlane_core/ui/ui" | ||
|
||
module Fastlane | ||
UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI) | ||
|
||
module Helper | ||
class GitHelper | ||
def self.setup_git_user(name: "Dax the Duck", email: "[email protected]") | ||
Actions.sh("echo \"git config --global user.name '#{name}'\"") | ||
Actions.sh("echo \"git config --global user.email '#{email}'\"") | ||
end | ||
|
||
def self.assert_main_branch(branch) | ||
unless self.assert_branch(branch, allowed_branches: ["main"]) | ||
UI.user_error!("Main branch required, got '#{branch}'.") | ||
end | ||
end | ||
|
||
def self.assert_release_or_hotfix_branch(branch) | ||
unless self.assert_branch(branch, allowed_branches: [%r{release/*}, %r{hotfix/*}]) | ||
UI.user_error!("Release or hotfix branch required, got '#{branch}'.") | ||
end | ||
end | ||
|
||
def self.assert_branch(branch, allowed_branches:) | ||
allowed_branches.any? { |allowed_branch| allowed_branch.match?(branch) } | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
describe Fastlane::Helper::GitHelper do | ||
describe "#assert_release_or_hotfix_branch" do | ||
it "accepts release or hotfix branch" do | ||
expect(Fastlane::UI).not_to receive(:user_error!) | ||
assert_release_or_hotfix_branch("release/1.0.0") | ||
assert_release_or_hotfix_branch("release/1") | ||
assert_release_or_hotfix_branch("hotfix/1.0.0") | ||
end | ||
|
||
it "shows error for non-release branch" do | ||
expect(Fastlane::UI).to receive(:user_error!).with("Release or hotfix branch required, got 'feature/foo'.") | ||
assert_release_or_hotfix_branch("feature/foo") | ||
end | ||
|
||
it "shows error for main branch" do | ||
expect(Fastlane::UI).to receive(:user_error!).with("Release or hotfix branch required, got 'feature/foo'.") | ||
assert_release_or_hotfix_branch("feature/foo") | ||
end | ||
|
||
def assert_release_or_hotfix_branch(branch) | ||
Fastlane::Helper::GitHelper.assert_release_or_hotfix_branch(branch) | ||
end | ||
end | ||
end |