From c1c2657085940a0ca11f21e609b477994e5f3a19 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 3 Sep 2024 22:47:56 +0200 Subject: [PATCH] Add partial spec --- .../actions/asana_find_release_task_action.rb | 66 ++++-- spec/asana_find_release_task_action_spec.rb | 196 ++++++++++++++++++ 2 files changed, 241 insertions(+), 21 deletions(-) create mode 100644 spec/asana_find_release_task_action_spec.rb diff --git a/lib/fastlane/plugin/ddg_apple_automation/actions/asana_find_release_task_action.rb b/lib/fastlane/plugin/ddg_apple_automation/actions/asana_find_release_task_action.rb index 2620e38..9cdffc4 100644 --- a/lib/fastlane/plugin/ddg_apple_automation/actions/asana_find_release_task_action.rb +++ b/lib/fastlane/plugin/ddg_apple_automation/actions/asana_find_release_task_action.rb @@ -3,6 +3,7 @@ require "httparty" require "json" require "octokit" +require "time" require_relative "../helper/ddg_apple_automation_helper" require_relative "../helper/github_actions_helper" @@ -37,19 +38,20 @@ def self.run(params) setup_constants(platform) latest_marketing_version = find_latest_marketing_version(github_token) - release_task_id = find_release_task(latest_marketing_version, platform, asana_access_token) + release_task_id = find_release_task(latest_marketing_version, asana_access_token) release_task_url = "#{Helper::DdgAppleAutomationHelper::ASANA_APP_URL}/#{release_task_id}/f" + release_branch = "release/#{latest_marketing_version}" UI.success("Found #{latest_marketing_version} release task: #{release_task_url}") - Helper::GitHubActionsHelper.set_output("release_branch", "release/#{latest_marketing_version}") + Helper::GitHubActionsHelper.set_output("release_branch", release_branch) Helper::GitHubActionsHelper.set_output("release_task_id", release_task_id) Helper::GitHubActionsHelper.set_output("release_task_url", release_task_url) { release_task_id: release_task_id, release_task_url: release_task_url, - release_branch: "release/#{latest_marketing_version}" + release_branch: release_branch } end @@ -58,13 +60,29 @@ def self.find_latest_marketing_version(github_token) # NOTE: `client.latest_release` returns release marked as "latest", i.e. a public release latest_internal_release = client.releases(@constants[:repo_name], { per_page: 1 }).first - tag_name = latest_internal_release['tag_name'] - version = tag_name&.split("-")&.first - Fastlane::UI.user_error!("Failed to fetch latest release") if version.nil? + + version = extract_version_from_tag_name(latest_internal_release&.tag_name) + if version.to_s.empty? + Fastlane::UI.user_error!("Failed to find latest marketing version") + return + end + unless self.validate_semver(version) + Fastlane::UI.user_error!("Invalid marketing version: #{version}, expected format: MAJOR.MINOR.PATCH") + return + end version end - def self.find_release_task(version, platform, asana_access_token) + def self.extract_version_from_tag_name(tag_name) + tag_name&.split("-")&.first + end + + def self.validate_semver(version) + # we only need basic "x.y.z" validation here + version.match?(/\A\d+\.\d+\.\d+\z/) + end + + def self.find_release_task(version, asana_access_token) release_task_name = "#{@constants[:release_task_prefix]} #{version}" # `completed_since=now` returns only incomplete tasks url = Helper::DdgAppleAutomationHelper::ASANA_API_URL + "/sections/#{@constants[:release_section_id]}/tasks?opt_fields=name,created_at&limit=100&completed_since=now" @@ -77,10 +95,16 @@ def self.find_release_task(version, platform, asana_access_token) loop do response = HTTParty.get(url, headers: { 'Authorization' => "Bearer #{asana_access_token}" }) - find_hotfix_task_in_response(response) - release_task_id ||= find_release_task_in_response(response, release_task_name) + unless response.success? + UI.user_error!("Failed to fetch release task: (#{response.code} #{response.message})") + return + end + parsed_response = response.parsed_response - url = response.dig('next_page', 'uri') + find_hotfix_task_in_response(parsed_response) + release_task_id ||= find_release_task_in_response(parsed_response, release_task_name) + + url = parsed_response.dig('next_page', 'uri') # Don't return as soon as release task is found, as we want to ensure there's no hotfix task break if url.nil? @@ -98,17 +122,6 @@ def self.find_release_task_in_response(response, release_task_name) release_task_id end - def self.find_hotfix_task_in_response(response) - hotfix_task_id = response['data'] - &.find { |task| task['name']&.start_with?(@constants[:hotfix_task_prefix]) } - &.dig('gid') - - if hotfix_task_id - UI.user_error!("Found active hotfix task: #{Helper::DdgAppleAutomationHelper::ASANA_API_URL}/#{hotfix_task_id}") - return - end - end - # Only consider release tasks created in the last 5 days. # - We don't want to bump internal release automatically for release tasks that are open for more than a week. # - The automatic check is only done Tuesday-Friday. If the release task is still open next Tuesday, it's unexpected, @@ -124,6 +137,17 @@ def self.ensure_task_not_too_old(release_task_id, created_at) end end + def self.find_hotfix_task_in_response(response) + hotfix_task_id = response['data'] + &.find { |task| task['name']&.start_with?(@constants[:hotfix_task_prefix]) } + &.dig('gid') + + if hotfix_task_id + UI.user_error!("Found active hotfix task: #{Helper::DdgAppleAutomationHelper::ASANA_API_URL}/#{hotfix_task_id}") + return + end + end + def self.description "Finds an active release task in Asana" end diff --git a/spec/asana_find_release_task_action_spec.rb b/spec/asana_find_release_task_action_spec.rb new file mode 100644 index 0000000..0e2015c --- /dev/null +++ b/spec/asana_find_release_task_action_spec.rb @@ -0,0 +1,196 @@ +describe Fastlane::Actions::AsanaFindReleaseTaskAction do + describe "#run" do + describe "when it finds the release task" do + it "returns release task ID, URL and release branch" do + expect(Fastlane::Actions::AsanaFindReleaseTaskAction).to receive(:find_latest_marketing_version).and_return("1.0.0") + expect(Fastlane::Actions::AsanaFindReleaseTaskAction).to receive(:find_release_task).and_return("1234567890") + allow(Fastlane::UI).to receive(:success) + allow(Fastlane::Helper::GitHubActionsHelper).to receive(:set_output) + + expect(test_action("ios")).to eq({ + release_task_id: "1234567890", + release_task_url: "https://app.asana.com/0/0/1234567890/f", + release_branch: "release/1.0.0" + }) + + expect(Fastlane::UI).to have_received(:success).with("Found 1.0.0 release task: https://app.asana.com/0/0/1234567890/f") + expect(Fastlane::Helper::GitHubActionsHelper).to have_received(:set_output).with("release_branch", "release/1.0.0") + expect(Fastlane::Helper::GitHubActionsHelper).to have_received(:set_output).with("release_task_id", "1234567890") + expect(Fastlane::Helper::GitHubActionsHelper).to have_received(:set_output).with("release_task_url", "https://app.asana.com/0/0/1234567890/f") + end + end + + def test_action(platform) + Fastlane::Actions::AsanaFindReleaseTaskAction.run(platform: platform) + end + end + + describe "#find_latest_marketing_version" do + it "returns the latest marketing version" do + client = double + allow(Octokit::Client).to receive(:new).and_return(client) + allow(client).to receive(:releases).and_return([double(tag_name: '1.0.0')]) + + expect(test_action).to eq("1.0.0") + end + + describe "when there is no latest release" do + it "shows error" do + client = double + allow(Octokit::Client).to receive(:new).and_return(client) + allow(client).to receive(:releases).and_return([]) + allow(Fastlane::UI).to receive(:user_error!) + + test_action + + expect(Fastlane::UI).to have_received(:user_error!).with("Failed to find latest marketing version") + end + end + + describe "when latest release is not a valid semver" do + it "shows error" do + client = double + allow(Octokit::Client).to receive(:new).and_return(client) + allow(client).to receive(:releases).and_return([double(tag_name: '1.0')]) + allow(Fastlane::UI).to receive(:user_error!) + + test_action + + expect(Fastlane::UI).to have_received(:user_error!).with("Invalid marketing version: 1.0, expected format: MAJOR.MINOR.PATCH") + end + end + + def test_action + Fastlane::Actions::AsanaFindReleaseTaskAction.find_latest_marketing_version("token") + end + end + + describe "#extract_version_from_tag_name" do + it "returns the version from the tag name" do + expect(test_action("1.0.0")).to eq("1.0.0") + expect(test_action("v1.0.0")).to eq("v1.0.0") + expect(test_action("1.105.0-251")).to eq("1.105.0") + end + + def test_action(tag_name) + Fastlane::Actions::AsanaFindReleaseTaskAction.extract_version_from_tag_name(tag_name) + end + end + + describe "#validate_semver" do + it "validates semantic version" do + expect(test_action("1.0.0")).to be_truthy + expect(test_action("0.0.0")).to be_truthy + expect(test_action("7.136.1")).to be_truthy + + expect(test_action("v1.0.0")).to be_falsy + expect(test_action("7.1")).to be_falsy + expect(test_action("1.105.0-251")).to be_falsy + expect(test_action("1005")).to be_falsy + end + + def test_action(version) + Fastlane::Actions::AsanaFindReleaseTaskAction.validate_semver(version) + end + end + + describe "#find_release_task" do + before do + Fastlane::Actions::AsanaFindReleaseTaskAction.setup_constants("ios") + end + + describe "when release task is found" do + before do + expect(HTTParty).to receive(:get).and_return( + double( + success?: true, + parsed_response: { 'data' => {}, 'next_page' => nil } + ) + ) + end + + it "returns the release task ID" do + expect(Fastlane::Actions::AsanaFindReleaseTaskAction).to receive(:find_hotfix_task_in_response) + expect(Fastlane::Actions::AsanaFindReleaseTaskAction).to receive(:find_release_task_in_response).and_return("1234567890") + + expect(test_action("1.0.0")).to eq("1234567890") + end + end + + describe "when fetching tasks in section fails" do + before do + expect(HTTParty).to receive(:get).and_return( + double( + success?: false, + code: 401, + message: "Unauthorized" + ) + ) + end + + it "shows error" do + expect(Fastlane::Actions::AsanaFindReleaseTaskAction).not_to receive(:find_hotfix_task_in_response) + expect(Fastlane::Actions::AsanaFindReleaseTaskAction).not_to receive(:find_release_task_in_response) + allow(Fastlane::UI).to receive(:user_error!) + + test_action("1.0.0") + + expect(Fastlane::UI).to have_received(:user_error!).with("Failed to fetch release task: (401 Unauthorized)") + end + end + + def test_action(version) + Fastlane::Actions::AsanaFindReleaseTaskAction.find_release_task(version, "token") + end + end + + describe "#find_release_task_in_response" do + describe "when release task is found" do + describe "when release task is not too old" do + before do + allow(Fastlane::Actions::AsanaFindReleaseTaskAction).to receive(:ensure_task_not_too_old) + end + + it "returns the release task ID" do + response = { 'data' => [{ 'name' => 'iOS App Release 1.0.0', 'gid' => '1234567890', 'created_at' => '2024-01-01' }] } + expect(test_action(response, "iOS App Release 1.0.0")).to eq("1234567890") + end + end + + describe "when release task is too old" do + before do + expect(Time).to receive(:now).and_return(Time.new(2024, 1, 10)).at_least(:once) + end + + it "shows error" do + allow(Fastlane::UI).to receive(:user_error!) + + response = { 'data' => [{ + 'name' => 'iOS App Release 1.0.0', + 'gid' => '1234567890', + 'created_at' => '2024-01-01' + }] } + test_action(response, "iOS App Release 1.0.0") + + expect(Fastlane::UI).to have_received(:user_error!).with("Found release task: 1234567890 but it's older than 5 days, skipping.") + end + end + end + + describe "when release task is not found" do + it "returns nil" do + expect(test_action({ 'data' => [{ 'name' => 'Release 1.0.0' }] }, "Release 1.0.1")).to be_nil + end + end + + def test_action(response, release_task_name) + Fastlane::Actions::AsanaFindReleaseTaskAction.find_release_task_in_response(response, release_task_name) + end + end + + describe "#find_hotfix_task_in_response" do + def test_action(response) + Fastlane::Actions::AsanaFindReleaseTaskAction.find_hotfix_task_in_response(response) + end + end +end