From 6e19404ef251b4638c616374e6ef16ceb838b9f3 Mon Sep 17 00:00:00 2001 From: Dejan K Date: Wed, 27 Aug 2025 15:23:31 +0200 Subject: [PATCH] fix(hooks_processor): add support for handling tag deletion hooks in Bitbucket worker --- hooks_processor/docker-compose.yml | 7 + .../hooks/payload/bitbucket.ex | 56 +++--- .../hooks/processing/bitbucket_worker.ex | 7 +- .../test/hooks/payload/bitbucket_test.exs | 31 +++- .../processing/bitbucket_worker_test.exs | 78 ++++++++ .../test/support/bitbucket_hooks.ex | 174 ++++++++++++++++++ 6 files changed, 325 insertions(+), 28 deletions(-) diff --git a/hooks_processor/docker-compose.yml b/hooks_processor/docker-compose.yml index f29dd6c16..417b90d02 100644 --- a/hooks_processor/docker-compose.yml +++ b/hooks_processor/docker-compose.yml @@ -39,6 +39,9 @@ services: volumes: - .:/app + - elixir_deps:/app/deps + - elixir_build:/app/_build + db: image: postgres:9.6 @@ -67,3 +70,7 @@ services: volumes: postgres-data: driver: local + elixir_deps: + driver: local + elixir_build: + driver: local diff --git a/hooks_processor/lib/hooks_processor/hooks/payload/bitbucket.ex b/hooks_processor/lib/hooks_processor/hooks/payload/bitbucket.ex index 405db7905..eba494758 100644 --- a/hooks_processor/lib/hooks_processor/hooks/payload/bitbucket.ex +++ b/hooks_processor/lib/hooks_processor/hooks/payload/bitbucket.ex @@ -19,7 +19,7 @@ defmodule HooksProcessor.Hooks.Payload.Bitbucket do end @doc """ - Used for concluding whether branch was created, updated or deleted via given push + Used for concluding whether branch or tag was created, updated or deleted via given push """ def branch_action(payload) do change = payload |> get_in(["push", "changes"]) |> Enum.at(0) @@ -47,41 +47,29 @@ defmodule HooksProcessor.Hooks.Payload.Bitbucket do """ def extract_data(payload, hook_type, action_type) + def extract_data(payload, "tag", "deleted") do + change = payload |> get_in(["push", "changes"]) |> Enum.at(0) |> Map.get("old") + extract_tag_data_(payload, change) + end + def extract_data(payload, "tag", _action_type) do change = payload |> get_in(["push", "changes"]) |> Enum.at(0) |> Map.get("new") - tag_name = Map.get(change, "name") - target = Map.get(change, "target") - repo_name = payload |> Map.get("repository") |> Map.get("name") - owner = payload |> get_in(["repository", "workspace", "slug"]) - author = get_in(target, ["author", "user", "nickname"]) - - %{ - branch_name: "refs/tags/" <> tag_name, - git_ref: "refs/tags/" <> tag_name, - display_name: tag_name, - owner: owner, - repo_name: repo_name, - commit_sha: Map.get(target, "hash"), - commit_message: Map.get(target, "message"), - commit_author: author, - pr_name: "", - pr_number: 0 - } + extract_tag_data_(payload, change) end def extract_data(payload, "branch", "deleted") do change = payload |> get_in(["push", "changes"]) |> Enum.at(0) |> Map.get("old") - extract_data_(payload, change) + extract_branch_data_(payload, change) end def extract_data(payload, "branch", _action_type) do change = payload |> get_in(["push", "changes"]) |> Enum.at(0) |> Map.get("new") - extract_data_(payload, change) + extract_branch_data_(payload, change) end - defp extract_data_(payload, change) do + defp extract_branch_data_(payload, change) do branch_name = Map.get(change, "name") target = Map.get(change, "target") repo_name = payload |> Map.get("repository") |> Map.get("name") @@ -105,6 +93,30 @@ defmodule HooksProcessor.Hooks.Payload.Bitbucket do } end + defp extract_tag_data_(payload, change) do + tag_name = Map.get(change, "name") + target = Map.get(change, "target") + repo_name = payload |> Map.get("repository") |> Map.get("name") + owner = payload |> get_in(["repository", "workspace", "slug"]) + + author = + get_in(target, ["author", "user", "nickname"]) || + get_in(payload, ["actor", "nickname"]) + + %{ + branch_name: "refs/tags/" <> tag_name, + git_ref: "refs/tags/" <> tag_name, + display_name: tag_name, + owner: owner, + repo_name: repo_name, + commit_sha: Map.get(target, "hash"), + commit_message: Map.get(target, "message"), + commit_author: author, + pr_name: "", + pr_number: 0 + } + end + @doc """ Extracts from payload provider's id of requester """ diff --git a/hooks_processor/lib/hooks_processor/hooks/processing/bitbucket_worker.ex b/hooks_processor/lib/hooks_processor/hooks/processing/bitbucket_worker.ex index 29afc0ec8..333b3132f 100644 --- a/hooks_processor/lib/hooks_processor/hooks/processing/bitbucket_worker.ex +++ b/hooks_processor/lib/hooks_processor/hooks/processing/bitbucket_worker.ex @@ -72,13 +72,14 @@ defmodule HooksProcessor.Hooks.Processing.BitbucketWorker do end defp process_webhook("tag", webhook, repository, requester_id) do - with parsed_data <- BBPayload.extract_data(webhook.request, "tag", "push"), + with action_type <- BBPayload.branch_action(webhook.request), + parsed_data <- BBPayload.extract_data(webhook.request, "tag", action_type), parsed_data <- Map.put(parsed_data, :yml_file, repository.pipeline_file), parsed_data <- Map.put(parsed_data, :requester_id, requester_id), parsed_data <- Map.put(parsed_data, :provider, "bitbucket"), {:skip_ci, false} <- BBPayload.skip_ci_flag?(parsed_data), {:build, true} <- should_build?(repository, parsed_data, :TAGS) do - perform_actions(webhook, parsed_data, "tag", "new") + perform_actions(webhook, parsed_data, "tag", action_type) else {:skip_ci, true, parsed_data} -> HooksQueries.update_webhook(webhook, parsed_data, "skip_ci") @@ -108,7 +109,7 @@ defmodule HooksProcessor.Hooks.Processing.BitbucketWorker do schedule_workflow(webhook, parsed_data) end - defp perform_actions(webhook, parsed_data, "branch", "deleted") do + defp perform_actions(webhook, parsed_data, hook_type, "deleted") when hook_type in ["branch", "tag"] do update_to_deleting_branch(webhook, parsed_data) end diff --git a/hooks_processor/test/hooks/payload/bitbucket_test.exs b/hooks_processor/test/hooks/payload/bitbucket_test.exs index 271c0e1ff..679650f42 100644 --- a/hooks_processor/test/hooks/payload/bitbucket_test.exs +++ b/hooks_processor/test/hooks/payload/bitbucket_test.exs @@ -24,7 +24,8 @@ defmodule HooksProcessor.Hooks.Payload.BitbucketTest do tag_hooks = [ BitbucketHooks.push_annoted_tag(), - BitbucketHooks.push_lightweight_tag() + BitbucketHooks.push_lightweight_tag(), + BitbucketHooks.tag_deletion() ] tag_hooks @@ -48,7 +49,9 @@ defmodule HooksProcessor.Hooks.Payload.BitbucketTest do test "branch_action() returns proper action type for all types of hooks" do branch_hooks = [ BitbucketHooks.push_new_branch_with_commits(), - BitbucketHooks.push_new_branch_no_commits() + BitbucketHooks.push_new_branch_no_commits(), + BitbucketHooks.push_annoted_tag(), + BitbucketHooks.push_lightweight_tag() ] branch_hooks @@ -66,7 +69,15 @@ defmodule HooksProcessor.Hooks.Payload.BitbucketTest do assert BBPayload.branch_action(hook) == "push" end) - assert BitbucketHooks.branch_deletion() |> BBPayload.branch_action() == "deleted" + branch_hooks = [ + BitbucketHooks.branch_deletion(), + BitbucketHooks.tag_deletion() + ] + + branch_hooks + |> Enum.each(fn hook -> + assert BBPayload.branch_action(hook) == "deleted" + end) end test "extract_data() returns valid data set for each type of the hook" do @@ -96,6 +107,18 @@ defmodule HooksProcessor.Hooks.Payload.BitbucketTest do assert data.pr_name == "" assert data.pr_number == 0 + data = BitbucketHooks.tag_deletion() |> BBPayload.extract_data("tag", "deleted") + assert data.branch_name == "refs/tags/v1.0-alpha" + assert data.git_ref == "refs/tags/v1.0-alpha" + assert data.display_name == "v1.0-alpha" + assert data.owner == "fake-test-user-1234" + assert data.repo_name == "fake-test-repo-2025" + assert data.commit_sha == "86efd1e2f788d237a9b8d6da5c04683d289ad805" + assert data.commit_message == "README.md created online with Bitbucket" + assert data.commit_author == "fake-test-user-1234" + assert data.pr_name == "" + assert data.pr_number == 0 + # Branches data = BitbucketHooks.push_new_branch_with_commits() |> BBPayload.extract_data("branch", "new") @@ -194,6 +217,8 @@ defmodule HooksProcessor.Hooks.Payload.BitbucketTest do assert BBPayload.extract_actor_id(BitbucketHooks.push_lightweight_tag()) == "{53c5afd4-936e-4ded-9b8a-398f527a33c9}" + assert BBPayload.extract_actor_id(BitbucketHooks.tag_deletion()) == "{00000000-6000-4000-9000-000000000012}" + assert BBPayload.extract_actor_id(BitbucketHooks.pull_request_open()) == "{53c5afd4-936e-4ded-9b8a-398f527a33c9}" assert BBPayload.extract_actor_id(BitbucketHooks.pull_request_closed()) == "{53c5afd4-936e-4ded-9b8a-398f527a33c9}" end diff --git a/hooks_processor/test/hooks/processing/bitbucket_worker_test.exs b/hooks_processor/test/hooks/processing/bitbucket_worker_test.exs index fdd561ad8..957decaa1 100644 --- a/hooks_processor/test/hooks/processing/bitbucket_worker_test.exs +++ b/hooks_processor/test/hooks/processing/bitbucket_worker_test.exs @@ -372,6 +372,84 @@ defmodule HooksProcessor.Hooks.Processing.BitbucketWorkerTest do GrpcMock.verify!(WorkflowServiceMock) end + test "valid tag-deleted hook => tag is archived" do + params = %{ + received_at: DateTime.utc_now(), + webhook: BitbucketHooks.tag_deletion(), + repository_id: UUID.uuid4(), + project_id: UUID.uuid4(), + organization_id: UUID.uuid4(), + provider: "bitbucket" + } + + assert {:ok, webhook} = HooksQueries.insert(params) + + # setup mocks + + ProjectHubServiceMock + |> GrpcMock.expect(:describe, fn req, _ -> + assert req.id == webhook.project_id + + %Projecthub.DescribeResponse{ + project: %{ + metadata: %{ + id: req.id, + org_id: UUID.uuid4() + }, + spec: %{ + repository: %{ + pipeline_file: ".semaphore/semaphore.yml", + run_on: [:BRANCHES, :TAGS], + whitelist: %{tags: ["/v1.*/", "/release-.*/"]} + } + } + }, + metadata: %{status: %{code: :OK}} + } + end) + + AdminServiceMock + |> GrpcMock.expect(:terminate_all, fn req, _ -> + assert req.project_id == webhook.project_id + assert req.branch_name == "refs/tags/v1.0-alpha" + assert req.reason == :BRANCH_DELETION + + %TerminateAllResponse{response_status: %{code: :OK}} + end) + + BranchServiceMock + |> GrpcMock.expect(:describe, fn req, _ -> + assert req.project_id == webhook.project_id + assert req.branch_name == "refs/tags/v1.0-alpha" + + %DescribeResponse{branch: %{id: webhook.id, name: "refs/tags/v1.0-alpha"}, status: %{code: :OK}} + end) + |> GrpcMock.expect(:archive, fn req, _ -> + assert req.branch_id == webhook.id + + %ArchiveResponse{status: %{code: :OK, message: "Success"}} + end) + + # wait for worker to finish and check results + + assert {:ok, pid} = WorkersSupervisor.start_worker_for_webhook(webhook.id) + + Test.Helpers.wait_for_worker_to_finish(pid, 15_000) + + assert {:ok, webhook} = HooksQueries.get_by_id(webhook.id) + assert webhook.state == "deleting_branch" + assert webhook.result == "OK" + assert webhook.wf_id == nil + assert webhook.ppl_id == nil + assert webhook.branch_id == webhook.id + assert webhook.commit_sha == "86efd1e2f788d237a9b8d6da5c04683d289ad805" + assert webhook.commit_author == "fake-test-user-1234" + assert webhook.git_ref == "refs/tags/v1.0-alpha" + + GrpcMock.verify!(ProjectHubServiceMock) + GrpcMock.verify!(BranchServiceMock) + end + test "[skip ci] flag in branch-push hook => hook in skip_ci state" do params = %{ received_at: DateTime.utc_now(), diff --git a/hooks_processor/test/support/bitbucket_hooks.ex b/hooks_processor/test/support/bitbucket_hooks.ex index 10c104e5f..3d68fa875 100644 --- a/hooks_processor/test/support/bitbucket_hooks.ex +++ b/hooks_processor/test/support/bitbucket_hooks.ex @@ -10,6 +10,7 @@ defmodule Support.BitbucketHooks do - push_commit - push_commit_force - branch_deletion + - tag_deletion - push_annoted_tag - push_lightweight_tag - pull_request_open @@ -2420,6 +2421,179 @@ defmodule Support.BitbucketHooks do } end + def tag_deletion do + %{ + "id" => "00000000-4000-4000-b000-000000000011", + "push" => %{ + "changes" => [ + %{ + "new" => nil, + "old" => %{ + "date" => "2025-08-27T11:17:57+00:00", + "name" => "v1.0-alpha", + "type" => "tag", + "links" => %{ + "html" => %{ + "href" => "https://bitbucket.org/fake-test-user-1234/fake-test-repo-2025/commits/tag/v1.0-alpha" + }, + "self" => %{ + "href" => + "https://api.bitbucket.org/2.0/repositories/fake-test-user-1234/fake-test-repo-2025/refs/tags/v1.0-alpha" + }, + "commits" => %{ + "href" => + "https://api.bitbucket.org/2.0/repositories/fake-test-user-1234/fake-test-repo-2025/commits/v1.0-alpha" + } + }, + "tagger" => %{}, + "target" => %{ + "date" => "2025-07-11T16:40:54+00:00", + "hash" => "86efd1e2f788d237a9b8d6da5c04683d289ad805", + "type" => "commit", + "links" => %{ + "html" => %{ + "href" => + "https://bitbucket.org/fake-test-user-1234/fake-test-repo-2025/commits/86efd1e2f788d237a9b8d6da5c04683d289ad805" + }, + "self" => %{ + "href" => + "https://api.bitbucket.org/2.0/repositories/fake-test-user-1234/fake-test-repo-2025/commit/86efd1e2f788d237a9b8d6da5c04683d289ad805" + } + }, + "author" => %{ + "raw" => "fake-test-user-1234 ", + "type" => "author", + "user" => %{ + "type" => "user", + "uuid" => "{10000000-5000-4000-9000-000000000012}", + "links" => %{ + "html" => %{ + "href" => "https://bitbucket.org/%7B10000000-5000-4000-9000-000000000012%7D/" + }, + "self" => %{ + "href" => "https://api.bitbucket.org/2.0/users/%7B10000000-5000-4000-9000-000000000012%7D" + }, + "avatar" => %{ + "href" => + "https://secure.gravatar.com/avatar/77922059a60e9df4e4896620745be781?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FO-3.png" + } + }, + "nickname" => "fake-test-user-1234", + "account_id" => "123456:90000000-e000-4000-b000-000000000012", + "display_name" => "fake-test-user-1234" + } + }, + "message" => "README.md created online with Bitbucket", + "parents" => [], + "summary" => %{ + "raw" => "README.md created online with Bitbucket", + "html" => "

README.md created online with Bitbucket

", + "type" => "rendered", + "markup" => "markdown" + }, + "rendered" => %{}, + "committer" => %{}, + "properties" => %{} + }, + "message" => "test tag description v1.0-alpha" + }, + "closed" => true, + "forced" => false, + "created" => false, + "truncated" => false + } + ] + }, + "actor" => %{ + "kind" => "repository_access_token", + "type" => "app_user", + "uuid" => "{00000000-6000-4000-9000-000000000012}", + "links" => %{ + "avatar" => %{ + "href" => + "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/123123:00000000-1000-4000-9000-000000000012/00000000-b000-4000-8000-000000000012/128" + } + }, + "account_id" => "123123:00000000-1000-4000-9000-000000000012", + "created_on" => "2025-08-27T11:39:07.919487+00:00", + "display_name" => "test-onprem-27082025", + "account_status" => "active" + }, + "repository" => %{ + "scm" => "git", + "name" => "fake-test-repo-2025", + "type" => "repository", + "uuid" => "{00000000-1000-4000-9000-000000000012}", + "links" => %{ + "html" => %{ + "href" => "https://bitbucket.org/fake-test-user-1234/fake-test-repo-2025" + }, + "self" => %{ + "href" => "https://api.bitbucket.org/2.0/repositories/fake-test-user-1234/fake-test-repo-2025" + }, + "avatar" => %{ + "href" => "https://bytebucket.org/ravatar/%7B00000000-1000-4000-9000-000000000012%7D?ts=default" + } + }, + "owner" => %{ + "type" => "team", + "uuid" => "{00000000-e000-4000-8000-000000000012}", + "links" => %{ + "html" => %{ + "href" => "https://bitbucket.org/%7B00000000-e000-4000-8000-000000000012%7D/" + }, + "self" => %{ + "href" => "https://api.bitbucket.org/2.0/workspaces/%7B00000000-e000-4000-8000-000000000012%7D" + }, + "avatar" => %{ + "href" => "https://bitbucket.org/account/fake-test-user-1234/avatar/" + } + }, + "username" => "fake-test-user-1234", + "display_name" => "fake-test-user-1234" + }, + "parent" => nil, + "project" => %{ + "key" => "ON", + "name" => "fake-test-repo-2025", + "type" => "project", + "uuid" => "{63b82869-0d52-4840-a528-4dc1ed07c496}", + "links" => %{ + "html" => %{ + "href" => "https://bitbucket.org/fake-test-user-1234/workspace/projects/ON" + }, + "self" => %{ + "href" => "https://api.bitbucket.org/2.0/workspaces/fake-test-user-1234/projects/ON" + }, + "avatar" => %{ + "href" => "https://bitbucket.org/fake-test-user-1234/workspace/projects/ON/avatar/32?ts=1752251119" + } + } + }, + "website" => nil, + "full_name" => "fake-test-user-1234/fake-test-repo-2025", + "workspace" => %{ + "name" => "fake-test-user-1234", + "slug" => "fake-test-user-1234", + "type" => "workspace", + "uuid" => "{00000000-e000-4000-8000-000000000012}", + "links" => %{ + "html" => %{ + "href" => "https://bitbucket.org/fake-test-user-1234/" + }, + "self" => %{ + "href" => "https://api.bitbucket.org/2.0/workspaces/fake-test-user-1234" + }, + "avatar" => %{ + "href" => "https://bitbucket.org/workspaces/fake-test-user-1234/avatar/?ts=1752251089" + } + } + }, + "is_private" => true + } + } + end + def push_lightweight_tag do %{ "actor" => %{