Skip to content

Commit

Permalink
WIP tag_release_action
Browse files Browse the repository at this point in the history
  • Loading branch information
ayoy committed Sep 13, 2024
1 parent 2b3be3b commit 915530b
Show file tree
Hide file tree
Showing 4 changed files with 353 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ def self.available_options
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

Expand Down
298 changes: 298 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,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 lib/fastlane/plugin/ddg_apple_automation/helper/git_helper.rb
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
24 changes: 24 additions & 0 deletions spec/git_helper_spec.rb
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

0 comments on commit 915530b

Please sign in to comment.