Skip to content

Commit

Permalink
Implement StartNewReleaseAction up to updating Asana tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
ayoy committed Sep 27, 2024
1 parent eb28f27 commit b0d4f03
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 45 deletions.
6 changes: 0 additions & 6 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ gem 'rubocop-require_tools'
# SimpleCov is a code coverage analysis tool for Ruby.
gem 'simplecov'

gem 'asana'
gem 'climate_control'
gem 'httparty'

gem 'octokit'

gemspec

plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
Expand Down
1 change: 1 addition & 0 deletions fastlane-plugin-ddg_apple_automation.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ Gem::Specification.new do |spec|
spec.add_dependency('climate_control')
spec.add_dependency('httpparty')
spec.add_dependency('octokit')
spec.add_dependency('semantic')
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require "fastlane/action"
require "fastlane_core/configuration/config_item"
require_relative "../helper/asana_helper"
require_relative "../helper/ddg_apple_automation_helper"
require_relative "../helper/git_helper"
require_relative "../helper/github_actions_helper"

Expand All @@ -9,49 +10,19 @@ module Actions
class StartNewReleaseAction < Action
def self.run(params)
Helper::GitHelper.setup_git_user
params[:platform] ||= Actions.lane_context[Actions::SharedValues::PLATFORM_NAME]
params[:asana_user_id] = Helper::AsanaHelper.get_asana_user_id_for_github_handle(params[:github_handle])

# macos_codefreeze_prechecks
# new_version = validate_new_version(options)
# macos_create_release_branch(version: new_version)
# macos_update_embedded_files
# macos_update_version_config(version: new_version)
# sh('git', 'push')
# sh("echo \"release_branch_name=#{RELEASE_BRANCH}/#{new_version}\" >> $GITHUB_OUTPUT") if is_ci
release_branch_name, new_version = create_release_branch(params)
params[:version] = new_version
params[:release_branch_name] = release_branch_name

# - name: Get Asana user ID
# id: get-asana-user-id
# shell: bash
# run: bundle exec fastlane run asana_get_user_id_for_github_handle github_handle:"${{ github.actor }}"
Helper::AsanaHelper.create_release_task(params[:platform], params[:version], params[:asana_user_id], params[:asana_access_token])

# - name: Create release task
# id: create_release_task
# env:
# ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
# ASSIGNEE_ID: ${{ steps.get-asana-user-id.outputs.asana_user_id }}
# run: |
# version="$(echo ${{ steps.make_release_branch.outputs.release_branch_name }} | cut -d '/' -f 2)"
# task_name="macOS App Release $version"
# asana_task_id="$(curl -fLSs -X POST "https://app.asana.com/api/1.0/task_templates/${{ vars.MACOS_RELEASE_TASK_TEMPLATE_ID }}/instantiateTask" \
# -H "Authorization: Bearer ${{ env.ASANA_ACCESS_TOKEN }}" \
# -H "Content-Type: application/json" \
# -d "{ \"data\": { \"name\": \"$task_name\" }}" \
# | jq -r .data.new_task.gid)"
# echo "marketing_version=${version}" >> $GITHUB_OUTPUT
# echo "asana_task_id=${asana_task_id}" >> $GITHUB_OUTPUT
# echo "asana_task_url=https://app.asana.com/0/0/${asana_task_id}/f" >> $GITHUB_OUTPUT

# curl -fLSs -X POST "https://app.asana.com/api/1.0/sections/${{ vars.MACOS_APP_DEVELOPMENT_RELEASE_SECTION_ID }}/addTask" \
# -H "Authorization: Bearer ${{ env.ASANA_ACCESS_TOKEN }}" \
# -H "Content-Type: application/json" \
# --output /dev/null \
# -d "{\"data\": {\"task\": \"${asana_task_id}\"}}"

# curl -fLSs -X PUT "https://app.asana.com/api/1.0/tasks/${asana_task_id}" \
# -H "Authorization: Bearer ${{ env.ASANA_ACCESS_TOKEN }}" \
# -H "Content-Type: application/json" \
# --output /dev/null \
# -d "{ \"data\": { \"assignee\": \"$ASSIGNEE_ID\" }}"
update_asana_tasks_for_release(params)
end

def self.update_asana_tasks_for_release(params)
# - name: Update Asana tasks for the release
# env:
# ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
Expand All @@ -64,6 +35,18 @@ def self.run(params)
# ${{ steps.create_release_task.outputs.marketing_version }}
end

def self.create_release_branch(params)
Helper::DdgAppleAutomationHelper.code_freeze_prechecks
new_version = Helper::DdgAppleAutomationHelper.validate_new_version(params[:version])
create_release_branch(new_version)
update_embedded_files(params)
update_version_config(new_version)
other_action.push_to_git_remote
Helper::GitHubActionsHelper.set_output("release_branch_name", "#{Helper::DdgAppleAutomationHelper::RELEASE_BRANCH}/#{new_version}")

return release_branch_name, new_version
end

def self.description
"Starts a new release"
end
Expand All @@ -89,9 +72,19 @@ def self.details

def self.available_options
[
FastlaneCore::ConfigItem.asana_access_token,
FastlaneCore::ConfigItem.platform,
FastlaneCore::ConfigItem.new(key: :version,
description: "Version number to force (calculated automatically if not provided)",
optional: true,
type: String),
FastlaneCore::ConfigItem.new(key: :release_task_template_id,
description: "Release task template ID",
optional: true,
type: String),
FastlaneCore::ConfigItem.new(key: :github_handle,
description: "Github user handle",
optional: false,
type: String)
]
end
Expand Down
74 changes: 74 additions & 0 deletions lib/fastlane/plugin/ddg_apple_automation/helper/asana_helper.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "fastlane_core/ui/ui"
require "asana"
require "httparty"
require_relative "ddg_apple_automation_helper"
require_relative "github_actions_helper"

Expand All @@ -8,10 +9,19 @@ module Fastlane

module Helper
class AsanaHelper
ASANA_API_URL = "https://app.asana.com/api/1.0"
ASANA_APP_URL = "https://app.asana.com/0/0"
ASANA_TASK_URL_REGEX = %r{https://app.asana.com/[0-9]/[0-9]+/([0-9]+)(:/f)?}
ERROR_ASANA_ACCESS_TOKEN_NOT_SET = "ASANA_ACCESS_TOKEN is not set"

IOS_HOTFIX_TASK_TEMPLATE_ID = "1205352950253153"
IOS_RELEASE_TASK_TEMPLATE_ID = "1205355281110338"
MACOS_HOTFIX_TASK_TEMPLATE_ID = "1206724592377782"
MACOS_RELEASE_TASK_TEMPLATE_ID = "1206127427850447"

IOS_APP_DEVELOPMENT_RELEASE_SECTION_ID = "1138897754570756"
MACOS_APP_DEVELOPMENT_RELEASE_SECTION_ID = "1202202395298964"

def self.asana_task_url(task_id)
if task_id.to_s.empty?
UI.user_error!("Task ID cannot be empty")
Expand Down Expand Up @@ -104,6 +114,70 @@ def self.upload_file_to_asana_task(task_id, file_path, asana_access_token)
end
end

def self.release_template_task_id(platform, is_hotfix: false)
case platform
when "ios"
is_hotfix ? IOS_HOTFIX_TASK_TEMPLATE_ID : IOS_RELEASE_TASK_TEMPLATE_ID
when "macos"
is_hotfix ? MACOS_HOTFIX_TASK_TEMPLATE_ID : MACOS_RELEASE_TASK_TEMPLATE_ID
else
UI.user_error!("Unsupported platform: #{platform}")
end
end

def self.release_task_name(version, platform, is_hotfix: false)
case platform
when "ios"
is_hotfix ? "iOS App Release #{version}" : "iOS App Hotfix Release #{version}"
when "macos"
is_hotfix ? "macOS App Release #{version}" : "macOS App Hotfix Release #{version}"
else
UI.user_error!("Unsupported platform: #{platform}")
end
end

def self.release_section_id(platform)
case platform
when "ios"
IOS_APP_DEVELOPMENT_RELEASE_SECTION_ID
when "macos"
MACOS_APP_DEVELOPMENT_RELEASE_SECTION_ID
else
UI.user_error!("Unsupported platform: #{platform}")
end
end

def self.create_release_task(platform, version, assignee_id, asana_access_token)
template_task_id = release_template_task_id(platform)
task_name = release_task_name(version, platform)
section_id = release_section_id(platform)

# task_templates is unavailable in the Asana client so we need to use the API directly
url = ASANA_API_URL + "/task_templates/#{template_task_id}/instantiateTask"
response = HTTParty.post(
url,
headers: { 'Authorization' => "Bearer #{asana_access_token}" },
body: { data: { name: task_name } }
)

if response.success?
task_id = response.parsed_response.dig('data', 'new_task', 'gid')
task_url = asana_task_url(task_id)
Helper::GitHubActionsHelper.set_output("asana_task_id", task_id)
Helper::GitHubActionsHelper.set_output("asana_task_url", task_url)
else
UI.user_error!("Failed to instantiate task from template #{template_task_id}: (#{response.code} #{response.message})")
end

asana_client = Asana::Client.new do |c|
c.authentication(:access_token, asana_access_token)
c.default_headers("Asana-Enable" => "new_goal_memberships,new_user_task_lists")
end

asana_client.sections.add_task_for_section(section_gid: section_id, task_gid: task_id)
asana_client.tasks.update_task(task_gid: task_id, data: { assignee: assignee_id })
end

def self.sanitize_asana_html_notes(content)
content.gsub(/\s+/, ' ') # replace multiple whitespaces with a single space
.gsub(/>\s+</, '><') # remove spaces between HTML tags
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "fastlane_core/configuration/config_item"
require "fastlane_core/ui/ui"
require "semantic"
require_relative "github_actions_helper"

module Fastlane
Expand All @@ -10,6 +11,163 @@ class DdgAppleAutomationHelper
ASANA_APP_URL = "https://app.asana.com/0/0"
ASANA_TASK_URL_REGEX = %r{https://app.asana.com/[0-9]/[0-9]+/([0-9]+)(:/f)?}

DEFAULT_BRANCH = 'main'
RELEASE_BRANCH = 'release'
HOTFIX_BRANCH = 'hotfix'

PROJECT_ROOT_FOLDER = File.dirname(File.expand_path(__dir__))
INFO_PLIST = File.join(PROJECT_ROOT_FOLDER, 'DuckDuckGo/Info.plist')
VERSION_CONFIG_PATH = File.join(PROJECT_ROOT_FOLDER, 'Configuration/Version.xcconfig')
BUILD_NUMBER_CONFIG_PATH = File.join(PROJECT_ROOT_FOLDER, 'Configuration/BuildNumber.xcconfig')
VERSION_CONFIG_DEFINITION = 'MARKETING_VERSION'
BUILD_NUMBER_CONFIG_DEFINITION = 'CURRENT_PROJECT_VERSION'

UPGRADABLE_EMBEDDED_FILES = {
ios: Set.new([
'Core/AppPrivacyConfigurationDataProvider.swift',
'Core/AppTrackerDataSetProvider.swift',
'Core/ios-config.json',
'Core/trackerData.json'
]),
macos: Set.new([
'DuckDuckGo/ContentBlocker/AppPrivacyConfigurationDataProvider.swift',
'DuckDuckGo/ContentBlocker/AppTrackerDataSetProvider.swift',
'DuckDuckGo/ContentBlocker/trackerData.json',
'DuckDuckGo/ContentBlocker/macos-config.json'
])
}.freeze

def self.code_freeze_prechecks
other_action.ensure_git_status_clean
other_action.ensure_git_branch(branch: DEFAULT_BRANCH)
other_action.git_pull

other_action.git_submodule_update(recursive: true, init: true)
other_action.ensure_git_status_clean
end

def self.validate_new_version(version)
current_version = current_version()
user_version = format_version(version)
new_version = user_version.nil? ? bump_minor_version(current_version) : user_version

UI.important("Current version in project settings is #{current_version}.")
UI.important("New version is #{new_version}.")

if UI.interactive? && !UI.confirm("Do you want to continue?")
UI.abort_with_message!('Aborted by user.')
end
new_version
end

def self.format_version(version)
user_version = nil

unless version.nil?
version_numbers = version.split('.')
version_numbers[3] = 0
version_numbers.map! { |element| element.nil? ? 0 : element }
user_version = "#{version_numbers[0]}.#{version_numbers[1]}.#{version_numbers[2]}"
end

user_version
end

# Updates version in the config file by bumping the minor (second) number
#
# @param [String] current version
# @return [String] updated version
#
def bump_minor_version(version)
Semantic::Version.new(version).increment!(:minor).to_s
end

# Updates version in the config file by bumping the patch (third) number
#
# @param [String] current version
# @return [String] updated version
#
def bump_patch_version(version)
Semantic::Version.new(version).increment!(:patch).to_s
end

# Reads build number from the config file
#
# @return [String] build number read from the file, or nil in case of failure
#
def self.current_build_number
current_build_number = 0

file_data = File.read(BUILD_NUMBER_CONFIG_PATH).split("\n")
file_data.each do |line|
current_build_number = line.split('=')[1].strip.to_i if line.start_with?(BUILD_NUMBER_CONFIG_DEFINITION)
end

current_build_number
end

# Updates version in the config file
#
# @return [String] version read from the file, or nil in case of failure
#
def self.current_version
current_version = nil

file_data = File.read(VERSION_CONFIG_PATH).split("\n")
file_data.each do |line|
current_version = line.split('=')[1].strip if line.start_with?(VERSION_CONFIG_DEFINITION)
end

current_version
end

def self.create_release_branch(version)
UI.message("Creating new release branch for #{version}")
release_branch = "#{RELEASE_BRANCH}/#{version}"

# Abort if the branch already exists
UI.abort_with_message!("Branch #{release_branch} already exists in this repository. Aborting.") unless Actions.sh(
'git', 'branch', '--list', release_branch
).empty?

# Create the branch and push
Actions.sh('git', 'checkout', '-b', release_branch)
Actions.sh('git', 'push', '-u', 'origin', release_branch)
end

def self.update_embedded_files(params)
Actions.sh("cd #{PROJECT_ROOT_FOLDER} && ./scripts/update_embedded.sh")

# Verify no unexpected files were modified
git_status = Actions.sh('git', 'status')
modified_files = git_status.split("\n").select { |line| line.include?('modified:') }
modified_files = modified_files.map { |str| str.split(':')[1].strip.delete_prefix('../') }

modified_files.each do |modified_file|
UI.abort_with_message!("Unexpected change to #{modified_file}.") unless UPGRADABLE_EMBEDDED_FILES[params[:platform]].any? do |s|
s.include?(modified_file)
end
end

# Run tests (CI will run them separately)
# run_tests(scheme: 'DuckDuckGo Privacy Browser') unless is_ci

# Everything looks good: commit and push
unless modified_files.empty?
modified_files.each { |modified_file| sh('git', 'add', modified_file.to_s) }
Actions.sh('git', 'commit', '-m', 'Update embedded files')
other_action.ensure_git_status_clean
end
end

def self.update_version_config(version)
File.write(VERSION_CONFIG_PATH, "#{VERSION_CONFIG_DEFINITION} = #{version}\n")
git_commit(
path: VERSION_CONFIG_PATH,
message: "Set marketing version to #{version}"
)
end

def self.process_erb_template(erb_file_path, args)
template_content = load_file(erb_file_path)
unless template_content
Expand Down

0 comments on commit b0d4f03

Please sign in to comment.