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
16 changes: 15 additions & 1 deletion src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ interface JiraProjectGroup {
* review your project scheme to see which transitions can be used to set the
* desired status.
*/
ValueMapping statuses();
StatusesValueMapping statuses();

/**
* Mapping of upstream issue types to downstream ones. Please make sure to
Expand Down Expand Up @@ -263,6 +263,20 @@ interface IssueTypeValueMapping extends ValueMapping {
Optional<String> epicLinkDestinationLabelCustomFieldName();
}

interface StatusesValueMapping extends ValueMapping {
/**
* @return The name of the resolution to apply to the "Close" transition when
* closing the issue deleted upstream before archiving it.
*/
Optional<String> deletedResolution();

/**
* @return The id of the transition to apply to get the "Close" transition when
* closing the issue deleted upstream before archiving it.
*/
Optional<String> deletedTransition();
}

interface UserValueMapping extends ValueMapping {
/**
* @return the name of the property to apply the assignee value to. With Jira
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.hibernate.infra.replicate.jira.JiraConfig;
import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestClient;
import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestClientBuilder;
import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueDeleteEventHandler;
import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueSimpleUpsertEventHandler;
import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueTransitionOnlyEventHandler;
import org.hibernate.infra.replicate.jira.service.jira.model.hook.JiraWebHookEvent;
Expand Down Expand Up @@ -157,6 +158,22 @@ public void registerManagementRoutes(@Observes ManagementInterface mi) {
triggerSyncEvent(context.sourceJiraClient().getIssue(issue), context);
rc.end();
});
mi.router().get("/sync/issues/deleted/:project").blockingHandler(rc -> {
String project = rc.pathParam("project");
String issues = rc.queryParam("issues").getFirst();

HandlerProjectContext context = contextPerProject.get(project);

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

String[] split = issues.split(",");
for (String key : split) {
context.submitTask(new JiraIssueDeleteEventHandler(reportingConfig, context, -1L, key));
}
rc.end();
});
mi.router().get("/sync/issues/transition/re-sync/:project").blockingHandler(rc -> {
// TODO: we can remove this one once we figure out why POST management does not
// work correctly...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ JiraIssues find(@QueryParam("jql") String query, @QueryParam("startAt") int star
@Path("/issue/{issueKey}/transitions")
void transition(@PathParam("issueKey") String issueKey, JiraTransition transition);

@PUT
@Path("/issue/{issueKey}/archive")
void archive(@PathParam("issueKey") String issueKey);

@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 @@ -249,6 +249,11 @@ public void transition(String issueKey, JiraTransition transition) {
withRetry(() -> delegate.transition(issueKey, transition));
}

@Override
public void archive(String issueKey) {
withRetry(() -> delegate.archive(issueKey));
}

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
@@ -1,12 +1,18 @@
package org.hibernate.infra.replicate.jira.service.jira.handler;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext;
import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestException;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraFields;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueTransition;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition;
import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig;

public class JiraIssueDeleteEventHandler extends JiraEventHandler {
Expand All @@ -21,45 +27,75 @@ public JiraIssueDeleteEventHandler(ReportingConfig reportingConfig, HandlerProje
@Override
protected void doRun() {
// TODO: do we actually want to delete the issue ? or maybe let's instead add a
// label,
// and update the summary, saying that the issue is deleted:
// label, and update the summary, saying that the issue is deleted:

// first let's make sure that the issue is actually deleted upstream:
try {
// Note: we do the search based on the jira key as we want to make sure
// that such key does not exist, searching by ID may also not find the issue,
// but then if the issue is not there we cannot check that the key matches the
// ID.
context.sourceJiraClient().getIssue(key);
JiraIssue issue = context.sourceJiraClient().getIssue(key);
if (issue != null && !key.equals(issue.key)) {
// means the issue got moved:
handleDeletedMovedIssue("MOVED (to %s)".formatted(issue.key));
return;
}

// if the issue is deleted then we should get a 404 and never reach this line:
failureCollector.critical("Request to delete an issue %s that is actually not deleted".formatted(key));
return;
} catch (JiraRestException e) {
// all good the issue is not available let's mark the other one as deleted now:
handleDeletedMovedIssue("DELETED");
}
}

try {
String destinationKey = toDestinationKey(key);
JiraIssue issue = context.destinationJiraClient().getIssue(destinationKey);
JiraIssue updated = new JiraIssue();

updated.fields = new JiraFields();
updated.fields.summary = "DELETED upstream: " + issue.fields.summary;
if (issue.fields.labels == null) {
issue.fields.labels = List.of();
}
ArrayList<String> updatedLabels = new ArrayList<>(issue.fields.labels);
updatedLabels.add("Deleted Upstream");
updated.fields.labels = updatedLabels;

context.destinationJiraClient().update(destinationKey, updated);
} catch (Exception ex) {
failureCollector.critical(
"Unable to mark the issue %s as deleted: %s".formatted(objectId, ex.getMessage()), ex);
private void handleDeletedMovedIssue(String type) {
try {
String destinationKey = toDestinationKey(key);
JiraIssue issue = context.destinationJiraClient().getIssue(destinationKey);
JiraIssue updated = new JiraIssue();

updated.fields = new JiraFields();
updated.fields.summary = "%s upstream: %s".formatted(type, issue.fields.summary);
if (issue.fields.labels == null) {
issue.fields.labels = List.of();
}
Set<String> updatedLabels = new HashSet<>(issue.fields.labels);
updatedLabels.add("deleted_upstream");
updated.fields.labels = new ArrayList<>(updatedLabels);
updated.fields.priority = null;
updated.fields.issuetype = null;
updated.fields.project = null;

context.destinationJiraClient().update(destinationKey, updated);

prepareTransition()
.ifPresent(transition -> context.destinationJiraClient().transition(destinationKey, transition));

context.destinationJiraClient().archive(destinationKey);
} catch (Exception ex) {
failureCollector.critical("Unable to mark the issue %s as deleted: %s".formatted(objectId, ex.getMessage()),
ex);
}
}

private Optional<JiraTransition> prepareTransition() {
Optional<String> deletedTransition = context.projectGroup().statuses().deletedTransition();
if (deletedTransition.isPresent()) {
JiraTransition transition = new JiraTransition();
transition.transition = new JiraIssueTransition(deletedTransition.get());

Optional<String> deletedResolution = context.projectGroup().statuses().deletedResolution();
deletedResolution.ifPresent(
name -> transition.properties().put("fields", Map.of("resolution", Map.of("name", name))));

return Optional.of(transition);
}

return Optional.empty();
}

@Override
public String toString() {
return "JiraIssueDeleteEventHandler[" + "key='" + key + '\'' + ", objectId=" + objectId + ", project="
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ public void transition(String issueKey, JiraTransition transition) {
// do nothing
}

@Override
public void archive(String issueKey) {
// do nothing
}

private JiraIssueLink sampleIssueLink(Long id) {
try {
return objectMapper.readValue("""
Expand Down
Loading