Skip to content

Commit

Permalink
Add tag_release_action
Browse files Browse the repository at this point in the history
  • Loading branch information
ayoy committed Sep 15, 2024
1 parent d106641 commit 69e0c06
Show file tree
Hide file tree
Showing 5 changed files with 352 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -171,14 +171,7 @@ def self.available_options
[
FastlaneCore::ConfigItem.asana_access_token,
FastlaneCore::ConfigItem.github_token,
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)

FastlaneCore::ConfigItem.platform
]
end

Expand Down
250 changes: 250 additions & 0 deletions lib/fastlane/plugin/ddg_apple_automation/actions/tag_release_action.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
require "fastlane/action"
require "fastlane_core/configuration/config_item"
require "octokit"
require_relative "../helper/asana_helper"
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 = {
dmg_url_prefix: "https://staticcdn.duckduckgo.com/macos-desktop-browser/",
repo_name: "duckduckgo/macos-browser"
}
end
end

def self.run(params)
platform = params[:platform] || Actions.lane_context[Actions::SharedValues::PLATFORM_NAME]
setup_constants(platform)

is_prerelease = params[:is_prerelease]

other_action.ensure_git_branch(branch: "^(:?release|hotfix)/.*$")
Helper::GitHelper.setup_git_user

tag_and_release_output = create_tag_and_github_release(is_prerelease, github_token)
Helper::GitHubActionsHelper.set_output("tag", tag_and_release_output[:tag])

begin
if is_prerelease
Helper::GitHelper.merge_branch(params[:branch], params[:base_branch], params[:github_elevated_permissions_token])
else
Helper::GitHelper.delete_branch(params[:branch], params[:github_token])
end
tag_and_release_output[:merge_or_delete_failed] = false
rescue StandardError
tag_and_release_output[:merge_or_delete_failed] = true
end

report_status(params.merge(tag_and_release_output))
end

def self.report_status(params)
template_args = {}
template_args['tag'] = params[:tag]
template_args['promoted_tag'] = params[:promoted_tag]
template_args['release_url'] = "https://github.com/#{@constants[:repo_name]}/releases/tag/#{tag}"
unless tag_created
template_args['last_release_tag'] = params[:latest_public_release_tag]
end
if params[:platform] == "macos"
dmg_version = (params[:is_prerelease] ? params[:tag] : params[:promoted_tag]).gsub('-', '.')
template_args['dmg_url'] = "#{@constants[:repo_name]}duckduckgo-#{dmg_version}.dmg"
end

task_template, comment_template = setup_asana_templates(params)

if task_template
AsanaCreateActionItemAction.run(
asana_access_token: params[:asana_access_token],
task_url: params[:asana_task_url],
template_name: task_template,
template_args: template_args,
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",
template_args: template_args,
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,
template_args: template_args,
is_scheduled_release: params[:is_scheduled_release]
)
end

def self.setup_asana_templates(params)
if params[:merge_or_delete_failed]
case [params[:tag_created], params[: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 = params[: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 = Helper::DdgAppleAutomationHelper.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.important("Failed to create and push tag: #{e}")
return {
tag: tag,
promoted_tag: promoted_tag,
tag_created: tag_created
}
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.important("Failed to create GitHub release: #{e}")
end

{
tag: tag,
promoted_tag: promoted_tag,
tag_created: tag_created,
latest_public_release_tag: latest_public_release.tag_name
}
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.platform,
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: :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)
]
end

def self.is_supported?(platform)
true
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ def self.process_erb_template(erb_file_path, args)
erb_template.result_with_hash(args)
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.path_for_asset_file(file)
File.expand_path("../assets/#{file}", __dir__)
end
Expand Down Expand Up @@ -66,5 +79,15 @@ def self.github_token
UI.user_error!("GITHUB_TOKEN is not set") if value.to_s.length == 0
end)
end

def self.platform
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
end
end
52 changes: 52 additions & 0 deletions lib/fastlane/plugin/ddg_apple_automation/helper/git_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require "fastlane/action"
require "fastlane_core/ui/ui"
require "octokit"

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

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.important("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.important("Failed to delete #{branch}: #{e}")
raise e
end
end
end
end
end
26 changes: 26 additions & 0 deletions spec/ddg_apple_automation_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,32 @@ def process_erb_template(erb_file_path, args)
end
end

describe "#compute_tag" do
describe "when is prerelease" do
let(:is_prerelease) { true }

it "computes tag and returns nil promoted tag" do
allow(File).to receive(:read).with("Configuration/Version.xcconfig").and_return("MARKETING_VERSION = 1.0.0")
allow(File).to receive(:read).with("Configuration/BuildNumber.xcconfig").and_return("CURRENT_PROJECT_VERSION = 123")
expect(compute_tag(is_prerelease)).to eq(["1.0.0-123", nil])
end
end

describe "when is public release" do
let(:is_prerelease) { false }

it "computes tag and promoted tag" do
allow(File).to receive(:read).with("Configuration/Version.xcconfig").and_return("MARKETING_VERSION = 1.0.0")
allow(File).to receive(:read).with("Configuration/BuildNumber.xcconfig").and_return("CURRENT_PROJECT_VERSION = 123")
expect(compute_tag(is_prerelease)).to eq(["1.0.0", "1.0.0-123"])
end
end

def compute_tag(is_prerelease)
Fastlane::Helper::DdgAppleAutomationHelper.compute_tag(is_prerelease)
end
end

describe "#load_file" do
it "shows error if provided file does not exist" do
allow(Fastlane::UI).to receive(:user_error!)
Expand Down

0 comments on commit 69e0c06

Please sign in to comment.