Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import java.util.regex.Pattern;

import org.hibernate.infra.replicate.jira.JiraConfig;
Expand All @@ -16,6 +20,7 @@
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueBulkResponse;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssues;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion;

import io.quarkus.logging.Log;

Expand All @@ -26,6 +31,7 @@ public final class HandlerProjectContext implements AutoCloseable {
private static final int ISSUES_PER_REQUEST = 25;
private static final String SYNC_ISSUE_PLACEHOLDER_SUMMARY = "Sync issue placeholder";
private final ReentrantLock lock = new ReentrantLock();
private final ReentrantLock versionLock = new ReentrantLock();

private final String projectName;
private final String projectGroupName;
Expand All @@ -43,6 +49,8 @@ public final class HandlerProjectContext implements AutoCloseable {
private final Pattern sourceLabelPattern;
private final DateTimeFormatter formatter;

private final Map<String, JiraVersion> destFixVersions;

public HandlerProjectContext(String projectName, String projectGroupName, JiraRestClient sourceJiraClient,
JiraRestClient destinationJiraClient, HandlerProjectGroupContext projectGroupContext,
Map<String, HandlerProjectContext> allProjectsContextMap) {
Expand All @@ -63,6 +71,9 @@ public HandlerProjectContext(String projectName, String projectGroupName, JiraRe
this.sourceLabelPattern = Pattern
.compile(projectGroupContext.projectGroup().formatting().labelTemplate().formatted(".+"));
this.formatter = DateTimeFormatter.ofPattern(projectGroupContext.projectGroup().formatting().timestampFormat());

this.destFixVersions = getAndCreateMissingCurrentFixVersions(project, projectGroupContext, sourceJiraClient,
destinationJiraClient);
}

public JiraConfig.JiraProject project() {
Expand Down Expand Up @@ -233,4 +244,107 @@ public boolean isSourceLabel(String label) {
public String formatTimestamp(ZonedDateTime time) {
return time != null ? time.format(formatter) : "";
}

public JiraVersion fixVersion(JiraVersion version) {
return fixVersion(version, false);
}

public JiraVersion fixVersion(JiraVersion version, boolean force) {
if (!force) {
JiraVersion v = destFixVersions.get(version.name);
if (v != null) {
return v;
}
}
versionLock.lock();
try {
if (force) {
return destFixVersions.compute(version.name, (name, current) -> upsert(project, projectGroupContext,
destinationJiraClient, version, List.of()));
} else {
return destFixVersions.computeIfAbsent(version.name,
name -> upsert(project, projectGroupContext, destinationJiraClient, version, List.of()));
}
} catch (Exception e) {
Log.errorf(e,
"Couldn't create a copy of the fix version %s, version will not be synced for a particular Jira ticket.",
version.name);
return null;
} finally {
versionLock.unlock();
}
}

public void refreshFixVersions() {
versionLock.lock();
try {
destFixVersions.clear();
destFixVersions.putAll(getAndCreateMissingCurrentFixVersions(project, projectGroupContext, sourceJiraClient,
destinationJiraClient));
} finally {
versionLock.unlock();
}
}

private static Map<String, JiraVersion> getAndCreateMissingCurrentFixVersions(JiraConfig.JiraProject project,
HandlerProjectGroupContext projectGroupContext, JiraRestClient sourceJiraClient,
JiraRestClient destinationJiraClient) {
Map<String, JiraVersion> result = new HashMap<>();

try {
List<JiraVersion> upstreamVersions = sourceJiraClient.versions(project.originalProjectKey());
List<JiraVersion> downstreamVersions = destinationJiraClient.versions(project.projectKey());

for (JiraVersion upstreamVersion : upstreamVersions) {
JiraVersion downstreamVersion = upsert(project, projectGroupContext, destinationJiraClient,
upstreamVersion, downstreamVersions);
result.put(upstreamVersion.name, downstreamVersion);
}
} catch (Exception e) {
Log.errorf(e, "Encountered a problem while building the fix version map for %s: %s", project.projectKey(),
e.getMessage());
}

return result;
}

private static JiraVersion upsert(JiraConfig.JiraProject project, HandlerProjectGroupContext projectGroupContext,
JiraRestClient jiraRestClient, JiraVersion upstreamVersion, List<JiraVersion> downstreamVersions) {
Optional<JiraVersion> version = JiraVersion.findVersion(upstreamVersion.id, downstreamVersions);
JiraVersion downstreamVersion = null;
if (version.isEmpty()) {
Log.infof("Creating a new fix version for project %s: %s", project.projectKey(), upstreamVersion.name);
downstreamVersion = processJiraVersion(project, projectGroupContext, upstreamVersion,
() -> jiraRestClient.create(upstreamVersion.copyForProject(project)));
} else if (versionNeedsUpdate(upstreamVersion, version.get())) {
Log.infof("Updating a fix version for project %s: %s", project.projectKey(), upstreamVersion.name);
downstreamVersion = processJiraVersion(project, projectGroupContext, upstreamVersion,
() -> jiraRestClient.update(version.get().id, upstreamVersion.copyForProject(project)));
} else {
downstreamVersion = version.get();
}
return downstreamVersion;
}

private static JiraVersion processJiraVersion(JiraConfig.JiraProject project,
HandlerProjectGroupContext projectGroupContext, JiraVersion upstreamVersion, Supplier<JiraVersion> action) {
try {
projectGroupContext.startProcessingEvent();
return action.get();
} catch (InterruptedException e) {
Log.error("Interrupted while trying to process fix version", e);
Thread.currentThread().interrupt();
} catch (Exception e) {
Log.errorf(e, "Ignoring fix version sync. Unable to process fix %s version for project %s: %s",
upstreamVersion.name, project.projectKey(), e.getMessage());
}
return null;
}

private static boolean versionNeedsUpdate(JiraVersion upstreamVersion, JiraVersion downstreamVersion) {
return !Objects.equals(upstreamVersion.name, downstreamVersion.name)
|| !Objects.equals(upstreamVersion.prepareVersionDescriptionForCopy(), downstreamVersion.description)
|| upstreamVersion.released != downstreamVersion.released
|| !Objects.equals(upstreamVersion.releaseDate, downstreamVersion.releaseDate);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,18 @@ public void registerManagementRoutes(@Observes ManagementInterface mi) {
triggerCommentSyncEvents(project, null, comments);
rc.end();
});
mi.router().get("/sync/fix-versions/:project").blockingHandler(rc -> {
String project = rc.pathParam("project");

HandlerProjectContext context = contextPerProject.get(project);

if (context == null) {
throw new IllegalArgumentException("Unknown project '%s'".formatted(project));
}

context.submitTask(context::refreshFixVersions);
rc.end();
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand Down Expand Up @@ -148,6 +149,22 @@ JiraIssues find(@QueryParam("jql") String query, @QueryParam("startAt") int star
@Path("/issue/{issueKey}/archive")
void archive(@PathParam("issueKey") String issueKey);

@GET
@Path("/version/{id}")
JiraVersion version(@PathParam("id") Long id);

@GET
@Path("/project/{projectKey}/versions")
List<JiraVersion> versions(@PathParam("projectKey") String projectKey);

@POST
@Path("/version")
JiraVersion create(JiraVersion version);

@PUT
@Path("/version/{id}")
JiraVersion update(@PathParam("id") String id, JiraVersion version);

@ClientObjectMapper
static ObjectMapper objectMapper(ObjectMapper defaultObjectMapper) {
return defaultObjectMapper.copy().setDefaultPropertyInclusion(JsonInclude.Include.NON_EMPTY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion;

import org.jboss.resteasy.reactive.RestResponse;
import org.jboss.resteasy.reactive.client.api.ClientLogger;
Expand Down Expand Up @@ -254,6 +255,26 @@ public void archive(String issueKey) {
withRetry(() -> delegate.archive(issueKey));
}

@Override
public JiraVersion version(Long id) {
return withRetry(() -> delegate.version(id));
}

@Override
public List<JiraVersion> versions(String projectKey) {
return withRetry(() -> delegate.versions(projectKey));
}

@Override
public JiraVersion create(JiraVersion version) {
return withRetry(() -> delegate.create(version));
}

@Override
public JiraVersion update(String id, JiraVersion version) {
return withRetry(() -> delegate.update(id, version));
}

private static final int RETRIES = 5;
private static final Duration WAIT_BETWEEN_RETRIES = Duration.of(2, ChronoUnit.SECONDS);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion;
import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig;

abstract class JiraIssueAbstractEventHandler extends JiraEventHandler {
Expand Down Expand Up @@ -163,6 +164,16 @@ protected JiraIssue issueToCreate(JiraIssue sourceIssue, JiraIssue downstreamIss
.put(epicLinkDestinationLabelCustomFieldName, sourceEpicLabel));
}

if (sourceIssue.fields.fixVersions != null) {
destinationIssue.fields.fixVersions = new ArrayList<>();
for (JiraVersion version : sourceIssue.fields.fixVersions) {
JiraVersion downstream = context.fixVersion(version);
if (downstream != null) {
destinationIssue.fields.fixVersions.add(downstream);
}
}
}

return destinationIssue;
}

Expand All @@ -175,7 +186,7 @@ private List<String> prepareLabels(JiraIssue sourceIssue, JiraIssue downstreamIs

// let's also add fix versions to the labels
if (sourceIssue.fields.fixVersions != null) {
for (JiraSimpleObject fixVersion : sourceIssue.fields.fixVersions) {
for (JiraVersion fixVersion : sourceIssue.fields.fixVersions) {
String fixVersionLabel = "Fix version:%s".formatted(fixVersion.name).replace(' ', '_');
labelsToSet.add(fixVersionLabel);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.hibernate.infra.replicate.jira.service.jira.handler;

import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion;
import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig;

public class JiraVersionUpsertEventHandler extends JiraEventHandler {

public JiraVersionUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long id) {
super(reportingConfig, context, id);
}

@Override
protected void doRun() {
JiraVersion version = context.sourceJiraClient().version(objectId);
context.fixVersion(version, true);
}

@Override
public String toString() {
return "JiraVersionUpsertEventHandler{" + "objectId=" + objectId + '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class JiraWebHookEvent extends JiraBaseObject {
public JiraWebHookObject comment;
public JiraWebHookIssue issue;
public JiraWebHookIssueLink issueLink;
public JiraWebHookObject version;

public Optional<JiraWebhookEventType> eventType() {
return JiraWebhookEventType.of(webhookEvent);
Expand All @@ -27,6 +28,6 @@ public Optional<JiraWebhookEventType> eventType() {
@Override
public String toString() {
return "JiraWebHookEvent{" + "webhookEvent='" + webhookEvent + '\'' + ", comment=" + comment + ", issue="
+ issue + ", otherProperties=" + properties() + '}';
+ issue + ", otherProperties=" + properties() + ", version=" + version + '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueLinkDeleteEventHandler;
import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueLinkUpsertEventHandler;
import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueUpsertEventHandler;
import org.hibernate.infra.replicate.jira.service.jira.handler.JiraVersionUpsertEventHandler;
import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig;

public enum JiraWebhookEventType {
Expand Down Expand Up @@ -119,6 +120,28 @@ public Collection<Runnable> handlers(ReportingConfig reportingConfig, JiraWebHoo
return List
.of(new JiraCommentDeleteEventHandler(reportingConfig, context, event.comment.id, event.issue.id));
}
},
VERSION_CREATED("jira:version_created") {
@Override
public Collection<Runnable> handlers(ReportingConfig reportingConfig, JiraWebHookEvent event,
HandlerProjectContext context) {
if (event.version == null || event.version.id == null) {
throw new IllegalStateException(
"Trying to handle a version event but version id is null: %s".formatted(event));
}
return List.of(new JiraVersionUpsertEventHandler(reportingConfig, context, event.version.id));
}
},
VERSION_UPDATED("jira:version_updated") {
@Override
public Collection<Runnable> handlers(ReportingConfig reportingConfig, JiraWebHookEvent event,
HandlerProjectContext context) {
if (event.version == null || event.version.id == null) {
throw new IllegalStateException(
"Trying to handle a version event but version id is null: %s".formatted(event));
}
return List.of(new JiraVersionUpsertEventHandler(reportingConfig, context, event.version.id));
}
};

private final String name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class JiraFields extends JiraBaseObject {

public JiraUser assignee;
public JiraUser reporter;
public List<JiraSimpleObject> fixVersions;
public List<JiraVersion> fixVersions;
// NOTE: this one is for "read-only" purposes, to create links a different API
// has to be used
public List<JiraIssueLink> issuelinks;
Expand Down
Loading
Loading