diff --git a/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java b/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java index f5a0fe7..c3e01be 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java @@ -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 @@ -263,6 +263,20 @@ interface IssueTypeValueMapping extends ValueMapping { Optional 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 deletedResolution(); + + /** + * @return The id of the transition to apply to get the "Close" transition when + * closing the issue deleted upstream before archiving it. + */ + Optional deletedTransition(); + } + interface UserValueMapping extends ValueMapping { /** * @return the name of the property to apply the assignee value to. With Jira diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java index e996fb2..04d78ca 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java @@ -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; @@ -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... diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java index 62c4b06..336daae 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java @@ -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); diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClientBuilder.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClientBuilder.java index 1814239..bad9877 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClientBuilder.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClientBuilder.java @@ -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); diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueDeleteEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueDeleteEventHandler.java index 485f831..d598177 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueDeleteEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueDeleteEventHandler.java @@ -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 { @@ -21,8 +27,7 @@ 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 { @@ -30,36 +35,67 @@ protected void doRun() { // 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 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 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 prepareTransition() { + Optional deletedTransition = context.projectGroup().statuses().deletedTransition(); + if (deletedTransition.isPresent()) { + JiraTransition transition = new JiraTransition(); + transition.transition = new JiraIssueTransition(deletedTransition.get()); + + Optional 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=" diff --git a/src/test/java/org/hibernate/infra/replicate/jira/mock/SampleJiraRestClient.java b/src/test/java/org/hibernate/infra/replicate/jira/mock/SampleJiraRestClient.java index 13c1e4f..3bde855 100644 --- a/src/test/java/org/hibernate/infra/replicate/jira/mock/SampleJiraRestClient.java +++ b/src/test/java/org/hibernate/infra/replicate/jira/mock/SampleJiraRestClient.java @@ -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("""