Skip to content

Commit

Permalink
feat(webhooks): Handle Bitbucket Server PR events (#1224)
Browse files Browse the repository at this point in the history
Backporting #1219 under a feature flag.

* Separate Bitbucket Server logic into its own class.
* Move Bitbucket Server Event classes into its own classes.

Bitbucket Server events:
* pr:opened
* pr:from_ref_updated
* pr:deleted
* pr:declined
  • Loading branch information
sergio-quintero committed Dec 6, 2022
1 parent fec5916 commit 6d1e3f9
Show file tree
Hide file tree
Showing 12 changed files with 1,486 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,26 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.spinnaker.echo.api.events.Event;
import com.netflix.spinnaker.echo.jackson.EchoObjectMapper;
import com.netflix.spinnaker.echo.scm.bitbucket.server.BitbucketServerEventHandler;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class BitbucketWebhookEventHandler implements GitWebhookHandler {

private ObjectMapper objectMapper;
private final ObjectMapper objectMapper;
private final Optional<BitbucketServerEventHandler> bitbucketServerEventHandler;

public BitbucketWebhookEventHandler() {
public BitbucketWebhookEventHandler(
@Nullable BitbucketServerEventHandler bitbucketServerEventHandler) {
this.objectMapper = EchoObjectMapper.getInstance();
this.bitbucketServerEventHandler = Optional.ofNullable(bitbucketServerEventHandler);
}

public boolean handles(String source) {
Expand Down Expand Up @@ -115,6 +121,10 @@ private boolean looksLikeBitbucketCloud(Event event) {
}

private boolean looksLikeBitbucketServer(Event event) {
if (bitbucketServerEventHandler.isPresent()) {
return bitbucketServerEventHandler.get().looksLikeBitbucketServer(event);
}

String eventType = event.content.get("event_type").toString();
return (eventType.equals("repo:refs_changed") || eventType.equals("pr:merged"));
}
Expand Down Expand Up @@ -193,6 +203,11 @@ private void handleBitbucketCloudEvent(Event event, Map postedEvent) {
}

private void handleBitbucketServerEvent(Event event, Map postedEvent) {
if (bitbucketServerEventHandler.isPresent()) {
bitbucketServerEventHandler.get().handleBitbucketServerEvent(event);
return;
}

String repoProject = "";
String slug = "";
String hash = "";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*
* Copyright 2022 Armory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.echo.scm.bitbucket.server;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.spinnaker.echo.api.events.Event;
import com.netflix.spinnaker.echo.jackson.EchoObjectMapper;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

@Component
@ConditionalOnProperty("webhooks.bitbucket.server.extras.enabled")
public class BitbucketServerEventHandler {

private final List<String> bitbucketServerEventTypes =
List.of(
"pr:opened",
"repo:refs_changed",
"pr:from_ref_updated",
"pr:merged",
"pr:declined",
"pr:deleted");

private String repoProject;
private String slug;
private String hash;
private String branch;

private final ObjectMapper objectMapper = EchoObjectMapper.getInstance();

public boolean looksLikeBitbucketServer(Event event) {
String eventType = event.content.get("event_type").toString();
return bitbucketServerEventTypes.contains(eventType);
}

public void handleBitbucketServerEvent(Event event) {

if (!event.content.containsKey("event_type")) {
return;
}

String eventType = event.content.get("event_type").toString();

switch (eventType) {
case "pr:opened":
handlePrOpenedEvent(event);
break;

case "repo:refs_changed":
handleRepoRefsChangedEvent(event);
break;

case "pr:from_ref_updated":
handlePrFromRefUpdatedEvent(event);
break;

case "pr:merged":
handlePrMergedEvent(event);
break;

case "pr:declined":
handlePrDeclinedEvent(event);
break;

case "pr:deleted":
handlePrDeletedEvent(event);
break;

default: // Do nothing
break;
}

event.content.put("repoProject", repoProject);
event.content.put("slug", slug);
event.content.put("hash", hash);
event.content.put("branch", branch);
event.content.put("action", eventType);
}

private void handlePrOpenedEvent(Event event) {
BitbucketServerPrEvent prOpenedEvent =
objectMapper.convertValue(event.content, BitbucketServerPrEvent.class);

if (prOpenedEvent.getPullRequest() != null
&& prOpenedEvent.getPullRequest().getFromRef() != null) {

BitbucketServerPrEvent.Ref fromRef = prOpenedEvent.getPullRequest().getFromRef();
branch = StringUtils.defaultIfEmpty(fromRef.getId(), "").replace("refs/heads/", "");

if (fromRef.getRepository() != null) {
repoProject = StringUtils.defaultIfEmpty(fromRef.getRepository().getProject().getKey(), "");
slug = StringUtils.defaultIfEmpty(fromRef.getRepository().getSlug(), "");
}

if (fromRef.getLatestCommit() != null) {
hash = StringUtils.defaultIfEmpty(fromRef.latestCommit, "");
}
}
}

private void handleRepoRefsChangedEvent(Event event) {
BitbucketServerRepoEvent refsChangedEvent =
objectMapper.convertValue(event.content, BitbucketServerRepoEvent.class);

if (refsChangedEvent.repository != null) {
repoProject = StringUtils.defaultIfEmpty(refsChangedEvent.repository.project.key, "");
slug = StringUtils.defaultIfEmpty(refsChangedEvent.repository.slug, "");
}

if (!refsChangedEvent.changes.isEmpty()) {
BitbucketServerRepoEvent.Change change = refsChangedEvent.changes.get(0);
hash = StringUtils.defaultIfEmpty(change.toHash, "");
if (change.ref != null) {
branch = StringUtils.defaultIfEmpty(change.ref.id, "").replace("refs/heads/", "");
}
}
}

private void handlePrFromRefUpdatedEvent(Event event) {
BitbucketServerPrEvent fromRefUpdatedEvent =
objectMapper.convertValue(event.content, BitbucketServerPrEvent.class);

if (fromRefUpdatedEvent.getPullRequest() != null
&& fromRefUpdatedEvent.getPullRequest().getFromRef() != null) {

BitbucketServerPrEvent.Ref fromRef = fromRefUpdatedEvent.getPullRequest().getFromRef();
branch = StringUtils.defaultIfEmpty(fromRef.getId(), "").replace("refs/heads/", "");

if (fromRef.getRepository() != null) {
repoProject = StringUtils.defaultIfEmpty(fromRef.getRepository().getProject().getKey(), "");
slug = StringUtils.defaultIfEmpty(fromRef.getRepository().getSlug(), "");
}

if (fromRef.getLatestCommit() != null) {
hash = StringUtils.defaultIfEmpty(fromRef.latestCommit, "");
}
}
}

private void handlePrMergedEvent(Event event) {
BitbucketServerPrEvent prMergedEvent =
objectMapper.convertValue(event.content, BitbucketServerPrEvent.class);

if (prMergedEvent.getPullRequest() != null && prMergedEvent.getPullRequest().toRef != null) {
BitbucketServerPrEvent.Ref toRef = prMergedEvent.getPullRequest().toRef;
branch = StringUtils.defaultIfEmpty(toRef.getId(), "").replace("refs/heads/", "");
if (toRef.getRepository() != null) {
repoProject = StringUtils.defaultIfEmpty(toRef.getRepository().getProject().getKey(), "");
slug = StringUtils.defaultIfEmpty(toRef.getRepository().getSlug(), "");
}
}

if (prMergedEvent.getPullRequest() != null
&& prMergedEvent.getPullRequest().getProperties() != null) {
BitbucketServerPrEvent.Properties properties = prMergedEvent.getPullRequest().getProperties();
if (properties.getMergeCommit() != null) {
hash = StringUtils.defaultIfEmpty(properties.getMergeCommit().getId(), "");
}
}
}

private void handlePrDeclinedEvent(Event event) {
BitbucketServerPrEvent prDeclinedEvent =
objectMapper.convertValue(event.content, BitbucketServerPrEvent.class);

if (prDeclinedEvent.getPullRequest() != null
&& prDeclinedEvent.getPullRequest().getFromRef() != null) {

BitbucketServerPrEvent.Ref fromRef = prDeclinedEvent.getPullRequest().getFromRef();
branch = StringUtils.defaultIfEmpty(fromRef.getId(), "").replace("refs/heads/", "");

if (fromRef.getRepository() != null) {
repoProject = StringUtils.defaultIfEmpty(fromRef.getRepository().getProject().getKey(), "");
slug = StringUtils.defaultIfEmpty(fromRef.getRepository().getSlug(), "");
}

if (fromRef.getLatestCommit() != null) {
hash = StringUtils.defaultIfEmpty(fromRef.latestCommit, "");
}
}
}

private void handlePrDeletedEvent(Event event) {
BitbucketServerPrEvent prDeletedEvent =
objectMapper.convertValue(event.content, BitbucketServerPrEvent.class);

if (prDeletedEvent.getPullRequest() != null
&& prDeletedEvent.getPullRequest().getFromRef() != null) {

BitbucketServerPrEvent.Ref fromRef = prDeletedEvent.getPullRequest().getFromRef();
branch = StringUtils.defaultIfEmpty(fromRef.getId(), "").replace("refs/heads/", "");

if (fromRef.getRepository() != null) {
repoProject = StringUtils.defaultIfEmpty(fromRef.getRepository().getProject().getKey(), "");
slug = StringUtils.defaultIfEmpty(fromRef.getRepository().getSlug(), "");
}

if (fromRef.getLatestCommit() != null) {
hash = StringUtils.defaultIfEmpty(fromRef.latestCommit, "");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2022 Armory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.echo.scm.bitbucket.server;

import lombok.Data;

@Data
public class BitbucketServerPrEvent {

private PullRequest pullRequest;

@Data
public static class PullRequest {
BitbucketServerPrEvent.Ref fromRef;
BitbucketServerPrEvent.Ref toRef;
BitbucketServerPrEvent.Properties properties;
}

@Data
public static class Ref {
String id;
String latestCommit;
BitbucketServerPrEvent.Repository repository;
}

@Data
public static class Project {
String key;
}

@Data
public static class Repository {
String name;
String slug;
Project project;
}

@Data
public static class Properties {
BitbucketServerPrEvent.MergeCommit mergeCommit;
}

@Data
public static class MergeCommit {
String id;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2022 Armory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.spinnaker.echo.scm.bitbucket.server;

import java.util.List;
import lombok.Data;

@Data
public class BitbucketServerRepoEvent {

List<Change> changes;
Repository repository;

@Data
public static class Project {
String key;
}

@Data
public static class Repository {
String name;
String slug;
BitbucketServerRepoEvent.Project project;
}

@Data
public static class Change {
public String toHash;
Ref ref;
}

@Data
public static class Ref {
String id;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ void canHandlePayload() throws IOException {
event.content = payload;
event.content.put("event_type", "repo:push");

BitbucketWebhookEventHandler handler = new BitbucketWebhookEventHandler();
BitbucketWebhookEventHandler handler = new BitbucketWebhookEventHandler(null);
assertThatCode(() -> handler.handle(event, payload)).doesNotThrowAnyException();
}
}

0 comments on commit 6d1e3f9

Please sign in to comment.