From 5f1d452f0c5f97e7493becaba537afeb953c1da4 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Mon, 10 Jun 2024 21:45:23 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B5=20=F0=9F=90=9B=20=F0=9F=AB=A3=20Cl?= =?UTF-8?q?ean=20up=20user=20modification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../github/context/DataCommonObject.java | 2 +- .../github/context/QueryContext.java | 38 ++++ .../automation/admin/api/ApplicationData.java | 35 +++- .../automation/admin/api/CommonhausUser.java | 4 +- .../admin/api/MemberAliasesResource.java | 21 ++- .../admin/api/MemberApplicationProcess.java | 106 +++++++++++ .../admin/api/MemberApplicationResource.java | 176 ++++++++++++------ .../admin/api/MemberAttestationResource.java | 31 ++- .../automation/admin/api/MemberResource.java | 17 +- .../admin/github/AdminGitHubEvents.java | 68 +++++-- .../admin/github/AppContextService.java | 89 ++------- .../admin/github/CommonhausDatastore.java | 135 ++++++++------ .../admin/github/ContextHelper.java | 22 ++- .../admin/github/TeamMemberSyncTest.java | 3 + 14 files changed, 517 insertions(+), 230 deletions(-) create mode 100644 cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberApplicationProcess.java diff --git a/bot-github-core/src/main/java/org/commonhaus/automation/github/context/DataCommonObject.java b/bot-github-core/src/main/java/org/commonhaus/automation/github/context/DataCommonObject.java index de8e065..547af12 100644 --- a/bot-github-core/src/main/java/org/commonhaus/automation/github/context/DataCommonObject.java +++ b/bot-github-core/src/main/java/org/commonhaus/automation/github/context/DataCommonObject.java @@ -77,7 +77,7 @@ public DataCommonObject(DataCommonObject other) { this.body = other.body; } - public Date mostRecent() { + public Date mostRecentEdit() { return lastEditedAt == null ? createdAt : lastEditedAt; } } diff --git a/bot-github-core/src/main/java/org/commonhaus/automation/github/context/QueryContext.java b/bot-github-core/src/main/java/org/commonhaus/automation/github/context/QueryContext.java index 79c5be5..8ea6183 100644 --- a/bot-github-core/src/main/java/org/commonhaus/automation/github/context/QueryContext.java +++ b/bot-github-core/src/main/java/org/commonhaus/automation/github/context/QueryContext.java @@ -21,6 +21,7 @@ import org.kohsuke.github.GHFileNotFoundException; import org.kohsuke.github.GHIOException; +import org.kohsuke.github.GHIssue; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHTeam; @@ -497,6 +498,22 @@ public DataCommonItem updateItemDescription(EventType eventType, String nodeId, }; } + public boolean closeIssue(GHIssue issue) { + if (isDryRun()) { + Log.debugf("[%s] closeIssue would close issue %s", getLogId(), issue.getNumber()); + return true; + } + execGitHubSync((gh, dryRun) -> { + issue.close(); + return null; + }); + if (hasErrors()) { + clearNotFound(); + return false; + } + return true; + } + public List queryReviews(String nodeId) { if (hasErrors()) { return List.of(); @@ -713,6 +730,27 @@ public Set collaborators(String repoFullName) { return collaborators; } + public void addTeamMember(GHUser user, String teamFullName) { + if (isDryRun()) { + Log.debugf("[%s] addTeamMember would add %s to %s", getLogId(), user.getLogin(), teamFullName); + return; + } + // this will trigger membership change events, which will come back around to update + // the cache. + String orgName = toOrganizationName(teamFullName); + String relativeName = toRelativeName(orgName, teamFullName); + GHOrganization org = getOrganization(orgName); + execGitHubSync((gh, dryRun) -> { + GHTeam ghTeam = org.getTeamByName(relativeName); + ghTeam.add(user); + return null; + }); + if (hasErrors()) { + clearNotFound(); + } + TEAM_MEMBERS.invalidate(teamFullName); + } + public String[] getErrorAddresses() { return ctx.botErrorEmailAddress(); } diff --git a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/ApplicationData.java b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/ApplicationData.java index 8bcb68a..24b7c61 100644 --- a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/ApplicationData.java +++ b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/ApplicationData.java @@ -6,6 +6,7 @@ import org.commonhaus.automation.admin.api.CommonhausUser.MembershipApplication; import org.commonhaus.automation.github.context.DataCommonComment; import org.commonhaus.automation.github.context.DataCommonItem; +import org.commonhaus.automation.github.context.DataLabel; import org.commonhaus.automation.markdown.MarkdownConverter; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -15,6 +16,10 @@ @RegisterForReflection public class ApplicationData { + public static final String NEW = "application/new"; + public static final String ACCEPTED = "application/accepted"; + public static final String DECLINED = "application/declined"; + static final Pattern CONTRIBUTIONS = Pattern.compile( "([\\s\\S]*?)([\\s\\S]*?)([\\s\\S]*?)", Pattern.CASE_INSENSITIVE); static final Pattern NOTES = Pattern.compile("([\\s\\S]*?)([\\s\\S]*?)([\\s\\S]*?)", @@ -30,9 +35,9 @@ public class ApplicationData { String additionalNotes; Feedback feedback; - public ApplicationData(MemberSession session, DataCommonItem issue) { + public ApplicationData(String login, DataCommonItem issue) { this.title = issue == null ? null : issue.title; - if (title == null || !ownerEquals(session.login())) { + if (title == null || !ownerEquals(login)) { return; } application = MembershipApplication.fromDataCommonType(issue); @@ -58,7 +63,7 @@ public void setFeedback(Feedback feedback) { } @JsonIgnore - public boolean isOwner() { + public boolean isValid() { return application != null; } @@ -84,7 +89,7 @@ public Feedback(DataCommonComment dataCommonComment) { String content = dataCommonComment.body.replaceAll("::response::", "").trim(); this.htmlContent = MarkdownConverter.toHtml(content); - date = dataCommonComment.mostRecent().toString(); + date = dataCommonComment.mostRecentEdit().toString(); } } @@ -92,6 +97,11 @@ public static String issueContent(MemberSession session, ApplicationPost applica return """ [%s](%s) + > [!TIP] + > - Include "::response::" in your comment to send feedback to the applicant. + > - Add the 'application/accepted' label to accept the application. + > - Add the 'application/declined' label to decline or reject the application. + ## Contribution Details %s @@ -112,12 +122,25 @@ public static String createTitle(MemberSession session) { return "Membership application: %s (%s)".formatted(session.name(), session.login()); } + public static String getLogin(DataCommonItem issue) { + return issue.title.replaceAll("Membership application: .*? \\((.*)\\)", "$1"); + } + + public static boolean isMemberApplicationEvent(DataCommonItem issue, DataLabel label) { + return issue.title.startsWith("Membership application:") + && (ACCEPTED.equals(label.name) || DECLINED.equals(label.name)); + } + public static boolean isUserFeedback(String body) { return body.contains("::response::"); } + public static boolean isAccepted(DataLabel label) { + return ACCEPTED.equals(label.name); + } + public static boolean isNewer(DataCommonComment x, Date issueMostRecent) { - Log.debugf("isNewer: %s %s", x.mostRecent(), issueMostRecent); - return x.mostRecent().after(issueMostRecent); + Log.debugf("isNewer: %s %s", x.mostRecentEdit(), issueMostRecent); + return x.mostRecentEdit().after(issueMostRecent); } } diff --git a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/CommonhausUser.java b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/CommonhausUser.java index d18e287..ae78439 100644 --- a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/CommonhausUser.java +++ b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/CommonhausUser.java @@ -36,7 +36,7 @@ public enum MemberStatus { ACTIVE, PENDING, INACTIVE, - DENIED, + DECLINED, REVOKED, SUSPENDED, SPONSOR, @@ -66,7 +66,7 @@ public boolean updateToPending() { || this == INACTIVE; } - public boolean updateToActive() { + public boolean updateFromPending() { return this == UNKNOWN || this == PENDING; } diff --git a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberAliasesResource.java b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberAliasesResource.java index f0192fb..e927c6b 100644 --- a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberAliasesResource.java +++ b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberAliasesResource.java @@ -21,6 +21,7 @@ import org.commonhaus.automation.admin.forwardemail.Alias; import org.commonhaus.automation.admin.github.AppContextService; import org.commonhaus.automation.admin.github.CommonhausDatastore; +import org.commonhaus.automation.admin.github.CommonhausDatastore.UpdateEvent; import io.quarkus.security.Authenticated; @@ -68,9 +69,7 @@ public Response getAliases(@DefaultValue("false") @QueryParam("refresh") boolean aliasMap = ctx.getAliases(emailAddresses, refresh); if (!forwardEmail.configured && !aliasMap.isEmpty()) { - forwardEmail.configured = true; - user = datastore.setCommonhausUser(user, session.roles(), - "Fix forward email service active flag", false); + user = updatedConfiguredFlag(user); } } return user.toResponse() @@ -112,9 +111,7 @@ public Response updateAliases(Map> aliases) { // Update alias mappings Map aliasMap = ctx.setRecipients(session.name(), aliases); if (!forwardEmail.configured && !aliasMap.isEmpty()) { - forwardEmail.configured = true; - user = datastore.setCommonhausUser(user, session.roles(), - "Fix forward email service active flag", false); + user = updatedConfiguredFlag(user); } return user.toResponse() .setData(ApiResponse.Type.ALIAS, aliasMap) @@ -127,6 +124,18 @@ public Response updateAliases(Map> aliases) { } } + CommonhausUser updatedConfiguredFlag(CommonhausUser user) { + // eventual consistency. No big deal if this + CommonhausUser result = datastore.setCommonhausUser(new UpdateEvent(user, + (c, u) -> { + u.services().forwardEmail().configured = true; + }, + "Fix forward email service active flag", + false, + false)); + return result == null ? user : result; + } + List getEmailAddresses(MemberSession session, ForwardEmail forwardEmail) { List addresses = new ArrayList<>(); addresses.add(session.login()); diff --git a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberApplicationProcess.java b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberApplicationProcess.java new file mode 100644 index 0000000..11451f7 --- /dev/null +++ b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberApplicationProcess.java @@ -0,0 +1,106 @@ +package org.commonhaus.automation.admin.api; + +import java.util.Date; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.commonhaus.automation.admin.api.ApplicationData.Feedback; +import org.commonhaus.automation.admin.api.CommonhausUser.MemberStatus; +import org.commonhaus.automation.admin.github.AppContextService; +import org.commonhaus.automation.admin.github.CommonhausDatastore; +import org.commonhaus.automation.admin.github.CommonhausDatastore.UpdateEvent; +import org.commonhaus.automation.admin.github.ScopedQueryContext; +import org.commonhaus.automation.github.context.DataCommonComment; +import org.commonhaus.automation.github.context.DataCommonItem; +import org.commonhaus.automation.github.context.DataLabel; +import org.commonhaus.automation.github.context.EventType; +import org.kohsuke.github.GHIssue; +import org.kohsuke.github.GHUser; + +@ApplicationScoped +public class MemberApplicationProcess { + + @Inject + AppContextService ctx; + + @Inject + CommonhausDatastore datastore; + + public void handleApplicationEvent(ScopedQueryContext qc, GHIssue issue, DataCommonItem item, DataLabel label) { + if (!ApplicationData.isMemberApplicationEvent(item, label)) { + return; + } + + String login = ApplicationData.getLogin(item); + GHUser applicant = qc.getUser(login); + CommonhausUser user = datastore.getCommonhausUser(login, applicant.getId(), false, false); + if (user == null) { + return; + } + + ApplicationData applicationData = new ApplicationData(login, item); + if (!applicationData.isValid()) { + // TODO: do we fix bad data from this side? (hasn't happened yet.. ) + ctx.logAndSendEmail(qc.getLogId(), "Invalid application data", + "Unable to find valid application data for login %s and issue %s (%s)" + .formatted(login, item.id, item.title), + null, null); + return; + } + + if (ApplicationData.isAccepted(label)) { + datastore.setCommonhausUser(new UpdateEvent(user, + (c, u) -> { + if (u.status().updateFromPending()) { + u.status(MemberStatus.ACTIVE); + } + u.application = null; + }, + "Membership application accepted", + true, + true)); + String teamFullName = ctx.getTeamForRole("member"); + if (teamFullName != null && !qc.hasErrors()) { + qc.addTeamMember(applicant, teamFullName); + } + } else { + datastore.setCommonhausUser(new UpdateEvent(user, + (c, u) -> { + if (u.status().updateFromPending()) { + u.status(MemberStatus.DECLINED); + } + }, + "Membership application declined", + true, + true)); + } + if (!qc.hasErrors()) { + qc.closeIssue(issue); + qc.removeLabels(item.id, List.of(ApplicationData.NEW)); + } + } + + ApplicationData findUserApplication(MemberSession session, String applicationId) { + ScopedQueryContext qc = ctx.getDatastoreContext(); + DataCommonItem issue = qc.getItem(EventType.issue, applicationId); + ApplicationData application = new ApplicationData(session.login(), issue); + if (application.isValid()) { + Feedback feedback = getFeedback(qc, applicationId, issue.mostRecentEdit()); + if (feedback != null) { + application.setFeedback(feedback); + } + } + return application; + } + + Feedback getFeedback(ScopedQueryContext qc, String nodeId, Date mostRecentEdit) { + List comments = qc.getComments(nodeId, + x -> ApplicationData.isUserFeedback(x.body) && ApplicationData.isNewer(x, mostRecentEdit)); + + return (comments == null || comments.isEmpty()) + ? null + : new Feedback(comments.get(0)); + } +} diff --git a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberApplicationResource.java b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberApplicationResource.java index 0f74af8..4bb77cb 100644 --- a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberApplicationResource.java +++ b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberApplicationResource.java @@ -1,5 +1,7 @@ package org.commonhaus.automation.admin.api; +import java.util.Collection; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import jakarta.enterprise.context.ApplicationScoped; @@ -17,10 +19,14 @@ import org.commonhaus.automation.admin.api.CommonhausUser.MembershipApplication; import org.commonhaus.automation.admin.github.AppContextService; import org.commonhaus.automation.admin.github.CommonhausDatastore; +import org.commonhaus.automation.admin.github.CommonhausDatastore.UpdateEvent; +import org.commonhaus.automation.admin.github.ScopedQueryContext; +import org.commonhaus.automation.github.context.DataCommonItem; +import org.commonhaus.automation.github.context.DataLabel; +import org.commonhaus.automation.github.context.EventType; import io.quarkus.logging.Log; import io.quarkus.security.Authenticated; -import io.smallrye.common.annotation.Blocking; @Path("/member/apply") @Authenticated @@ -35,6 +41,9 @@ public class MemberApplicationResource { @Inject MemberSession session; + @Inject + MemberApplicationProcess memberApplicationProcess; + @GET @KnownUser @Produces("application/json") @@ -44,24 +53,16 @@ public Response getApplication() { if (user == null) { return Response.status(Response.Status.NOT_FOUND).build(); } + MembershipApplication application = user.application(); ApplicationData applicationData = application == null ? null - : ctx.getOpenApplication(session, application.nodeId()); + : memberApplicationProcess.findUserApplication(session, application.nodeId()); - if (applicationData == null || !applicationData.isOwner()) { - if (application != null) { - user.application = null; - String state = applicationData == null ? "not found" : "not owner"; - updateStatus(user, "Remove membership application (" + state + ")", false); - } + if (application == null && applicationData == null) { return Response.status(Response.Status.NOT_FOUND).build(); - } - - if (user.status().updateToPending()) { - // compensate for missing status - user.status(MemberStatus.PENDING); - user = updateStatus(user, "Set status to PENDING", true); + } else if (applicationData == null || !applicationData.isValid() || user.status().updateToPending()) { + return doUserApplicationUpdate(user, applicationData, null); // WRITE } return user.toResponse() .setData(Type.APPLY, applicationData) @@ -76,63 +77,124 @@ public Response getApplication() { @KnownUser @Produces("application/json") public Response setApplication(ApplicationPost applicationPost) { - AtomicBoolean checkRunning = AdminDataCache.APPLICATION_CHECK.computeIfAbsent(session.login(), - (k) -> new AtomicBoolean(false)); + try { + if (applicationPost == null) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + CommonhausUser user = datastore.getCommonhausUser(session, false, false); + if (user == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + MembershipApplication application = user.application(); + ApplicationData applicationData = application == null + ? null + : memberApplicationProcess.findUserApplication(session, application.nodeId()); - // There are a few separate updates here (dealing with the issue AND (separately) with the user record). - // We'll enforce one at a time coming from the UI for this one. - return checkRunning.compareAndSet(false, true) - ? doApplicationUpdate(applicationPost, checkRunning) - : Response.status(Response.Status.TOO_MANY_REQUESTS).build(); + if (applicationData != null && !applicationData.isValid()) { + applicationData = null; + } + return doUserApplicationUpdate(user, applicationData, applicationPost); // WRITE + } catch (Throwable e) { + Log.errorf(e, "getApplication: Unable to retrieve application for %s: %s", session.login(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } } - @Blocking - private CommonhausUser updateStatus(CommonhausUser user, String message, boolean history) { + private Response doUserApplicationUpdate(CommonhausUser user, ApplicationData applicationData, + ApplicationPost post) { AtomicBoolean checkRunning = AdminDataCache.APPLICATION_CHECK.computeIfAbsent(session.login(), (k) -> new AtomicBoolean(false)); - boolean iAmWriter = checkRunning.compareAndSet(false, true); - if (iAmWriter) { + + if (checkRunning.compareAndSet(false, true)) { try { - return datastore.setCommonhausUser(user, session.roles(), message, history); + ScopedQueryContext qc = ctx.getDatastoreContext(); + + boolean notFound = applicationData == null && post == null; + boolean notOwner = applicationData != null && !applicationData.isValid(); + + if (notFound || notOwner) { + // RESET/REMOVE APPLICATION FROM USER (missing or bad, no replacement) + String state = notFound ? "not found" : "not owner"; + user = datastore.setCommonhausUser(new UpdateEvent(user, + (c, u) -> { + u.application = null; + }, + "Remove membership application (" + state + ")", + false, + true)); + + return Response.status(Response.Status.NOT_FOUND).build(); + } else if (post == null) { + // UPDATING MISMATCHED STATUS + if (user.status().updateToPending()) { + user = datastore.setCommonhausUser(new UpdateEvent(user, + (c, u) -> { + if (u.status().updateToPending()) { + u.status(MemberStatus.PENDING); + } + }, + "Set status to PENDING", + false, + true)); + } + return user.toResponse() + .setData(Type.APPLY, applicationData) + .finish(); + } + + // UPDATE APPLICATION ISSUE + ApplicationData updated = userUpdateApplicationIssue(qc, applicationData, post); + + if (qc.hasErrors()) { + Throwable e = qc.bundleExceptions(); + qc.clearErrors(); + ctx.logAndSendEmail(qc.getLogId(), "Failed to update MembershipApplication issue", e, null); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + if (updated == null) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + final MembershipApplication application = updated.application; + user = datastore.setCommonhausUser(new UpdateEvent(user, + (c, u) -> { + u.application = application; + if (u.status().updateToPending()) { + u.status(MemberStatus.PENDING); + } + }, + "Updated membership application", + false, + true)); + + return user.toResponse() + .setData(Type.APPLY, updated) + .finish(); } finally { checkRunning.set(false); } + } else { + return Response.status(Response.Status.TOO_MANY_REQUESTS).build(); } - return user; } - @Blocking - private Response doApplicationUpdate(ApplicationPost applicationPost, AtomicBoolean checkRunning) { - try { - CommonhausUser user = datastore.getCommonhausUser(session, false, false); - if (user == null) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - ApplicationData applicationData = ctx.updateApplication(session, user, applicationPost); - if (applicationData == null) { - return Response.status(Response.Status.BAD_REQUEST).build(); - } + public ApplicationData userUpdateApplicationIssue(ScopedQueryContext qc, + ApplicationData applicationData, + ApplicationPost applicationPost) { - user.application = applicationData.application; - if (user.status().updateToPending()) { - user.status(MemberStatus.PENDING); - } - user = datastore.setCommonhausUser(user, session.roles(), "Created membership application", false); + String content = ApplicationData.issueContent(session, applicationPost); + Collection labels = qc.findLabels(List.of(ApplicationData.NEW)); + MembershipApplication application = applicationData == null ? null : applicationData.application; - if (user.postConflict()) { // on conflict, user is reset with value from repo - // retry once. - user.application = applicationData.application; - user = datastore.setCommonhausUser(user, session.roles(), "Created membership application", false); - } + DataCommonItem item = application == null + ? qc.createItem(EventType.issue, + ApplicationData.createTitle(session), + content, + labels) + : qc.updateItemDescription(EventType.issue, application.nodeId(), content); - return user.toResponse() - .setData(Type.APPLY, applicationData) - .finish(); - } catch (Throwable e) { - Log.errorf(e, "doApplicationUpdate: Unable to update user application for %s: %s", session.login(), e); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); - } finally { - checkRunning.set(false); - } + return item == null + ? null + : new ApplicationData(session.login(), item); } + } diff --git a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberAttestationResource.java b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberAttestationResource.java index 03f0b63..cbeb5ab 100644 --- a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberAttestationResource.java +++ b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberAttestationResource.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -18,6 +19,7 @@ import org.commonhaus.automation.admin.api.CommonhausUser.MemberStatus; import org.commonhaus.automation.admin.github.AppContextService; import org.commonhaus.automation.admin.github.CommonhausDatastore; +import org.commonhaus.automation.admin.github.CommonhausDatastore.UpdateEvent; import io.quarkus.logging.Log; import io.quarkus.security.Authenticated; @@ -44,14 +46,22 @@ public Response updateAttestation(AttestationPost post) { return Response.status(Response.Status.BAD_REQUEST).build(); } try { + final Set roles = session.roles(); CommonhausUser user = datastore.getCommonhausUser(session, false, true); - user.updateMemberStatus(ctx, session.roles()); + user.updateMemberStatus(ctx, roles); Attestation newAttestation = createAttestation(user.status(), post); - user.goodUntil().attestation.put(post.id(), newAttestation); String message = "Sign attestation (%s|%s)".formatted(post.id(), post.version()); - user = datastore.setCommonhausUser(user, session.roles(), message, true); + user = datastore.setCommonhausUser(new UpdateEvent(user, + (c, u) -> { + u.updateMemberStatus(c, roles); + u.goodUntil().attestation.put(post.id(), newAttestation); + }, + message, + true, + true)); + return user.toResponse().finish(); } catch (Throwable e) { Log.errorf(e, "updateAttestation: Unable to update attestation for %s: %s", session.login(), e); @@ -69,8 +79,9 @@ public Response updateAttestations(List postList) { } try { + final Set roles = session.roles(); CommonhausUser user = datastore.getCommonhausUser(session, false, true); - user.updateMemberStatus(ctx, session.roles()); + user.updateMemberStatus(ctx, roles); Map newAttestations = new HashMap<>(); @@ -79,8 +90,16 @@ public Response updateAttestations(List postList) { newAttestations.put(p.id(), createAttestation(user.status(), p)); message.append("(%s|%s) ".formatted(p.id(), p.version())); } - user.goodUntil().attestation.putAll(newAttestations); - user = datastore.setCommonhausUser(user, session.roles(), message.toString(), true); + + user = datastore.setCommonhausUser(new UpdateEvent(user, + (c, u) -> { + u.updateMemberStatus(c, roles); + u.goodUntil().attestation.putAll(newAttestations); + }, + message.toString(), + true, + true)); + return user.toResponse().finish(); } catch (Throwable e) { Log.errorf(e, "updateAttestations: Unable to update attestations for %s: %s", session.login(), e); diff --git a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberResource.java b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberResource.java index 1b2d4b0..9919f11 100644 --- a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberResource.java +++ b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/api/MemberResource.java @@ -1,6 +1,7 @@ package org.commonhaus.automation.admin.api; import java.net.URI; +import java.util.Set; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -16,6 +17,7 @@ import org.commonhaus.automation.admin.AdminDataCache; import org.commonhaus.automation.admin.github.AppContextService; import org.commonhaus.automation.admin.github.CommonhausDatastore; +import org.commonhaus.automation.admin.github.CommonhausDatastore.UpdateEvent; import org.eclipse.microprofile.config.inject.ConfigProperty; import io.quarkus.logging.Log; @@ -73,7 +75,6 @@ public Response finishLogin() { public Response getUserInfo(@DefaultValue("false") @QueryParam("refresh") boolean refresh) { if (refresh) { AdminDataCache.KNOWN_USER.invalidate(session.login()); - AdminDataCache.COMMONHAUS_DATA.invalidate(CommonhausDatastore.getKey(session)); session.userIsKnown(ctx); } @@ -90,6 +91,9 @@ public Response getUserInfo(@DefaultValue("false") @QueryParam("refresh") boolea @Path("/commonhaus") @Produces("application/json") public Response getCommonhausUser(@DefaultValue("false") @QueryParam("refresh") boolean refresh) { + if (refresh) { + AdminDataCache.COMMONHAUS_DATA.invalidate(CommonhausDatastore.getKey(session)); + } try { CommonhausUser user = datastore.getCommonhausUser(session, refresh, true); @@ -110,10 +114,15 @@ public Response getCommonhausUser(@DefaultValue("false") @QueryParam("refresh") public Response updateUserStatus() { try { CommonhausUser user = datastore.getCommonhausUser(session, false, false); - if (user.updateMemberStatus(ctx, session.roles())) { + final Set roles = session.roles(); + if (user.updateMemberStatus(ctx, roles)) { // Refresh the user's status - user = datastore.setCommonhausUser(user, session.roles(), - "Update roles: %s".formatted(session.roles()), true); + user = datastore.setCommonhausUser(new UpdateEvent( + user, + (c, u) -> u.updateMemberStatus(c, roles), + "Update member status", + false, + false)); } return user.toResponse().finish(); } catch (Exception e) { diff --git a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/github/AdminGitHubEvents.java b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/github/AdminGitHubEvents.java index 82d5658..08904f1 100644 --- a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/github/AdminGitHubEvents.java +++ b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/github/AdminGitHubEvents.java @@ -4,16 +4,19 @@ import java.util.Objects; import jakarta.inject.Inject; +import jakarta.json.JsonObject; import org.commonhaus.automation.admin.AdminDataCache; +import org.commonhaus.automation.admin.api.MemberApplicationProcess; import org.commonhaus.automation.admin.config.UserManagementConfig.AttestationConfig; import org.commonhaus.automation.github.context.ActionType; import org.commonhaus.automation.github.context.DataCommonComment; import org.commonhaus.automation.github.context.DataCommonItem; import org.commonhaus.automation.github.context.DataLabel; -import org.commonhaus.automation.github.context.EventType; +import org.commonhaus.automation.github.context.JsonAttribute; import org.kohsuke.github.GHEventPayload; import org.kohsuke.github.GHIssue; +import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; @@ -31,6 +34,9 @@ public class AdminGitHubEvents { @Inject AppContextService ctx; + @Inject + MemberApplicationProcess applicationProcess; + public void updateAttestationList(GitHub github, DynamicGraphQLClient graphQLClient, @Push GHEventPayload.Push pushEvent) { AttestationConfig cfg = ctx.attestationConfig(); @@ -57,35 +63,67 @@ public void updateAttestationList(GitHub github, DynamicGraphQLClient graphQLCli public void updateMembership(GitHub github, DynamicGraphQLClient graphQLClient, @Membership GHEventPayload.Membership membershipEvent) { AdminDataCache.KNOWN_USER.invalidate(membershipEvent.getMember().getLogin()); - ScopedQueryContext qc = ctx.refreshScopedQueryContext( - github, - membershipEvent.getRepository(), - membershipEvent.getInstallation().getId()) - .addExisting(graphQLClient); + GHRepository repo = membershipEvent.getRepository(); + GHOrganization org = membershipEvent.getOrganization(); + + ScopedQueryContext qc = ctx.getScopedQueryContext( + repo == null ? org.getLogin() : repo.getFullName()); + if (qc == null) { + return; + } + qc.addExisting(github).addExisting(graphQLClient); qc.updateTeamList(membershipEvent.getOrganization(), membershipEvent.getTeam()); } - public void updateApplications(GitHubEvent event, GitHub github, DynamicGraphQLClient graphQLClient, - @Issue GHEventPayload.Issue issueEvent) { + /** + * Called when an issue is labeled + * + * @param event + * @param github + * @param graphQLClient + * @param issueCommentEvent + */ + public void updateApplication(GitHubEvent event, GitHub github, DynamicGraphQLClient graphQLClient, + @Issue.Labeled GHEventPayload.Issue issueEvent) { String repoFullName = issueEvent.getRepository().getFullName(); + ActionType actionType = ActionType.fromString(event.getAction()); + + // ignore if it isn't an issue in the datastore repository if (!repoFullName.equals(ctx.getDataStore())) { return; } + ScopedQueryContext qc = ctx.refreshScopedQueryContext( github, issueEvent.getRepository(), issueEvent.getInstallation().getId()) .addExisting(graphQLClient); - GHIssue issue = issueEvent.getIssue(); - String nodeId = issue.getNodeId(); - DataCommonItem item = qc.getItem(EventType.issue, nodeId); + JsonObject payload = JsonAttribute.unpack(event.getPayload()); + DataCommonItem issue = JsonAttribute.issue.commonItemFrom(payload); + DataLabel label = JsonAttribute.label.labelFrom(payload); - Log.debugf("[%s] updateApplications: %s", qc.getLogId(), - issue.getNumber(), item); + Log.debugf("[%s] updateApplication #%s - %s", qc.getLogId(), + issue.number, actionType); + + try { + applicationProcess.handleApplicationEvent(qc, issueEvent.getIssue(), issue, label); + } catch (Exception e) { + ctx.logAndSendEmail(qc.getLogId(), "Error with issue label event", e, null); + } finally { + qc.clearErrors(); + } } + /** + * Called when there is a comment event on an issue. + * + * @param event + * @param github + * @param graphQLClient + * @param issueCommentEvent + */ public void updateApplicationComments(GitHubEvent event, GitHub github, DynamicGraphQLClient graphQLClient, @IssueComment GHEventPayload.IssueComment issueCommentEvent) { String repoFullName = issueCommentEvent.getRepository().getFullName(); @@ -102,12 +140,12 @@ public void updateApplicationComments(GitHubEvent event, GitHub github, DynamicG String nodeId = issue.getNodeId(); List comments = qc.getComments(nodeId, x -> true); - Log.debugf("[%s] updateApplications: %s", qc.getLogId(), + Log.debugf("[%s] updateApplicationComments: %s", qc.getLogId(), issue.getNumber(), comments); } /** - * Called when there is event. + * Called when there is a label change (added or removed from the repository) event. * * @param event GitHubEvent (raw payload) * @param github GitHub API (connection instance) diff --git a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/github/AppContextService.java b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/github/AppContextService.java index a5871ff..6c28d9c 100644 --- a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/github/AppContextService.java +++ b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/github/AppContextService.java @@ -3,8 +3,6 @@ import java.io.IOException; import java.net.URI; import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -20,12 +18,7 @@ import org.commonhaus.automation.admin.AdminConfig; import org.commonhaus.automation.admin.AdminDataCache; -import org.commonhaus.automation.admin.api.ApplicationData; -import org.commonhaus.automation.admin.api.ApplicationData.ApplicationPost; -import org.commonhaus.automation.admin.api.ApplicationData.Feedback; -import org.commonhaus.automation.admin.api.CommonhausUser; import org.commonhaus.automation.admin.api.CommonhausUser.MemberStatus; -import org.commonhaus.automation.admin.api.CommonhausUser.MembershipApplication; import org.commonhaus.automation.admin.api.MemberSession; import org.commonhaus.automation.admin.config.AdminConfigFile; import org.commonhaus.automation.admin.config.UserManagementConfig; @@ -34,10 +27,6 @@ import org.commonhaus.automation.admin.forwardemail.ForwardEmailClient; import org.commonhaus.automation.config.BotConfig; import org.commonhaus.automation.github.context.BaseContextService; -import org.commonhaus.automation.github.context.DataCommonComment; -import org.commonhaus.automation.github.context.DataCommonItem; -import org.commonhaus.automation.github.context.DataLabel; -import org.commonhaus.automation.github.context.EventType; import org.commonhaus.automation.github.context.QueryContext; import org.commonhaus.automation.github.discovery.RepositoryDiscoveryEvent; import org.eclipse.microprofile.rest.client.inject.RestClient; @@ -401,6 +390,17 @@ public MemberStatus getStatusForRole(String role) { return MemberStatus.fromString(status); } + public String getTeamForRole(String role) { + if (userConfig.isDisabled()) { + return null; + } + return userConfig.teamRoles().entrySet().stream() + .filter(entry -> entry.getValue().equals(role)) + .map(Entry::getKey) + .findFirst() + .orElse(null); + } + /** * Event filter: check if the push event contains changes to the specified path * @@ -430,71 +430,4 @@ public static ObjectMapper yamlMapper() { } return yamlMapper; } - - public ApplicationData getOpenApplication(MemberSession session, String applicationId) { - if (applicationId == null) { - return null; - } - ScopedQueryContext qc = getDatastoreContext(); - DataCommonItem issue = qc.getItem(EventType.issue, applicationId); - - ApplicationData application = new ApplicationData(session, issue); - if (application.isOwner()) { - Feedback feedback = getFeedback(qc, applicationId, issue.mostRecent()); - if (feedback != null) { - application.setFeedback(feedback); - } - } - return application; - } - - public ApplicationData updateApplication(MemberSession session, CommonhausUser user, - ApplicationPost applicationPost) throws Throwable { - if (applicationPost == null) { - return null; - } - - MembershipApplication application = user.application(); - if (application != null) { - ApplicationData existing = getOpenApplication(session, application.nodeId()); - if (existing != null && !existing.isOwner()) { - application = null; - } - } - - ScopedQueryContext qc = getDatastoreContext(); - String content = ApplicationData.issueContent(session, applicationPost); - Collection labels = qc.findLabels(List.of("new-member")); - if (labels.isEmpty()) { - // TODO: config for labels / repo discovery - DataLabel newLabel = qc.createLabel("new-member", "#78A658"); - if (newLabel != null) { - labels = List.of(newLabel); - } - } - - DataCommonItem item = application == null - ? qc.createItem(EventType.issue, - ApplicationData.createTitle(session), - content, - labels) - : qc.updateItemDescription(EventType.issue, application.nodeId(), content); - - if (qc.hasErrors()) { - throw qc.bundleExceptions(); - } - - return item == null - ? null - : new ApplicationData(session, item); - } - - Feedback getFeedback(ScopedQueryContext qc, String nodeId, Date mostRecent) { - List comments = qc.getComments(nodeId, - x -> ApplicationData.isUserFeedback(x.body) && ApplicationData.isNewer(x, mostRecent)); - - return (comments == null || comments.isEmpty()) - ? null - : new Feedback(comments.get(0)); - } } diff --git a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/github/CommonhausDatastore.java b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/github/CommonhausDatastore.java index b428fcf..2d3a6d5 100644 --- a/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/github/CommonhausDatastore.java +++ b/cf-admin-bot/src/main/java/org/commonhaus/automation/admin/github/CommonhausDatastore.java @@ -1,8 +1,8 @@ package org.commonhaus.automation.admin.github; import java.io.IOException; -import java.util.Set; import java.util.concurrent.Executor; +import java.util.function.BiConsumer; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -44,17 +44,29 @@ interface DatastoreEvent { String login(); - Set roles(); - boolean create(); } - public record QueryEvent(String login, long id, Set roles, boolean refresh, + public record QueryEvent(String login, long id, boolean refresh, boolean create) implements DatastoreEvent { } - public record UpdateEvent(CommonhausUser user, String message, Set roles, - boolean history) implements DatastoreEvent { + /** + * Update Commonhaus user data + * + * @param user Commonhaus user object + * @param updateUser Function to apply changes to the user. This function will not have access to the MemberSession. + * @param message Commit message + * @param history Whether to add the message to the user's history + * @param retry Whether to retry the update if there is a conflict + */ + public record UpdateEvent( + CommonhausUser user, + BiConsumer updateUser, + String message, + boolean history, + boolean retry) implements DatastoreEvent { + @Override public long id() { return user.id(); @@ -69,6 +81,14 @@ public String login() { public boolean create() { return true; } + + public void applyChanges(AppContextService ctx, CommonhausUser user) { + updateUser.accept(ctx, user); + } + + static UpdateEvent retryEvent(UpdateEvent initial, CommonhausUser revisedUser) { + return new UpdateEvent(revisedUser, initial.updateUser(), initial.message(), initial.history(), false); + } } public CommonhausUser getCommonhausUser(MemberSession session) { @@ -82,9 +102,19 @@ public CommonhausUser getCommonhausUser(MemberSession session) { * @throws RuntimeException if GitHub or other API query fails */ public CommonhausUser getCommonhausUser(MemberSession session, boolean resetCache, boolean create) { - QueryEvent query = new QueryEvent(session.login(), session.id(), session.roles(), resetCache, create); + return getCommonhausUser(session.login(), session.id(), resetCache, create); + } + + /** + * GET Commonhaus user data + * + * @return A Commonhaus user object (never null) + * @throws RuntimeException if GitHub or other API query fails + */ + public CommonhausUser getCommonhausUser(String login, long id, boolean resetCache, boolean create) { + QueryEvent query = new QueryEvent(login, id, resetCache, create); Message response = ctx.getBus().requestAndAwait(CommonhausDatastore.READ, query); - Log.debugf("[getCommonhausUser|%s] Get Commonhaus user data: %s", session.id(), response.body()); + Log.debugf("[getCommonhausUser|%s] Get Commonhaus user data: %s", login, response.body()); return response.body(); } @@ -96,10 +126,9 @@ public CommonhausUser getCommonhausUser(MemberSession session, boolean resetCach * * @throws RuntimeException if GitHub or other API query fails */ - public CommonhausUser setCommonhausUser(CommonhausUser user, Set roles, String message, boolean history) { - UpdateEvent update = new UpdateEvent(user, message, roles, history); - Message response = ctx.getBus().requestAndAwait(CommonhausDatastore.WRITE, update); - Log.debugf("[setCommonhausUser|%s] Update Commonhaus user data: %s", user.id(), response.body()); + public CommonhausUser setCommonhausUser(UpdateEvent updateEvent) { + Message response = ctx.getBus().requestAndAwait(CommonhausDatastore.WRITE, updateEvent); + Log.debugf("[setCommonhausUser|%s] Update Commonhaus user data: %s", updateEvent.login(), response.body()); return response.body(); } @@ -157,7 +186,6 @@ public Uni fetchCommonhausUser(QueryEvent event) { @ConsumeEvent(value = WRITE) public Uni pushCommonhausUser(UpdateEvent event) { final CommonhausUser user = event.user(); - final String key = getKey(user); CommonhausUser result = null; ScopedQueryContext qc = ctx.getDatastoreContext(); @@ -166,8 +194,7 @@ public Uni pushCommonhausUser(UpdateEvent event) { ctx.logAndSendEmail("pushCommonhausUser", "Unable to get datastore query context", e, null); return Uni.createFrom().failure(e); } else if (!qc.hasErrors()) { - GHRepository repo = qc.getRepository(); - result = updateCommonhausUser(qc, repo, user, event, key); + result = updateCommonhausUser(qc, event); } if (qc.hasErrors()) { @@ -182,54 +209,62 @@ public Uni pushCommonhausUser(UpdateEvent event) { return Uni.createFrom().item(() -> u).emitOn(executor); } - private CommonhausUser updateCommonhausUser(ScopedQueryContext qc, GHRepository repo, - CommonhausUser input, UpdateEvent event, String key) { - - CommonhausUser result; + private CommonhausUser updateCommonhausUser(ScopedQueryContext qc, UpdateEvent event) { + GHRepository repo = qc.getRepository(); + CommonhausUser user = deepCopy(event.user()); // leave original alone + String key = getKey(user); + // Callback: Apply changes to the user + event.applyChanges(ctx, user); if (event.history()) { - input.addHistory(event.message()); + user.addHistory(event.message()); } - String content = writeUser(qc, input); + if (qc.isDryRun()) { + return user; + } + + String content = writeUser(qc, user); if (content == null) { - // If it can't be serialized, bail. - return input; + // If it can't be serialized, bail and return the original + return event.user(); } + GHContentBuilder update = repo.createContent() - .path(dataPath(input.id())) - .message("🤖 [%s] %s".formatted(input.id(), event.message())) + .path(dataPath(user.id())) + .message("🤖 [%s] %s".formatted(user.id(), event.message())) .content(content); - if (input.sha() != null) { - update.sha(input.sha()); + if (user.sha() != null) { + update.sha(user.sha()); } - GHContentUpdateResponse response = qc.execGitHubSync((gh3, dryRun3) -> { - if (dryRun3) { - return null; - } - return update.commit(); - }); - + GHContentUpdateResponse response = qc.execGitHubSync((gh3, dryRun3) -> update.commit()); HttpException ex = qc.getConflict(); if (ex != null) { qc.clearConflict(); - Log.debugf("[%s|%s] Conflict updating Commonhaus user data: %s", - qc.getLogId(), input.id(), ex.getResponseMessage()); + Log.debugf("[%s|%s] Conflict updating Commonhaus user data", qc.getLogId(), user.login()); // we're here after a save conflict; re-read the data - result = readCommonhausUser(qc, repo, event, key); - if (result != null) { - // recovered from conflict w/o further IOException - // Otherwise, query context will still contain errors, see caller - result.setConflict(true); + user = readCommonhausUser(qc, repo, event, key); + if (user != null) { + if (event.retry()) { + // retry the update + return updateCommonhausUser(qc, UpdateEvent.retryEvent(event, user)); + } else { + // allow caller to handle unresolved conflict + user.setConflict(true); + } } - } else { + } else if (!qc.hasErrors()) { GHContent responseContent = response.getContent(); - result = parseUser(qc, input, responseContent); + user = parseUser(qc, responseContent); + if (user != null) { + AdminDataCache.COMMONHAUS_DATA.put(key, deepCopy(user)); + } } - return result; + // Caller should be check for query context errors + return user; } /** Get user data: will return null on IOException (including not found) */ @@ -272,14 +307,12 @@ private CommonhausUser deepCopy(CommonhausUser user) { return user; } - private CommonhausUser parseUser(ScopedQueryContext qc, CommonhausUser user, GHContent responseContent) { + private CommonhausUser parseUser(ScopedQueryContext qc, GHContent responseContent) { try { return CommonhausUser.parseFile(qc, responseContent); } catch (IOException e) { - // unlikely, but safer to keep what we had - // send an email to the admin because this shouldn't happen. - ctx.logAndSendEmail("CommonhausDatastore.parseUser", "Unable to deserialize Commonhaus user", e, null); - return user; + qc.addException(e); + return null; } } @@ -287,9 +320,7 @@ private String writeUser(ScopedQueryContext qc, CommonhausUser input) { try { return qc.writeValue(input); } catch (IOException e) { - // unlikely, but allow us to fail fast without exceptions everyplace - // send an email to the admin because this shouldn't happen. - ctx.logAndSendEmail("CommonhausDatastore.writeUser", "Unable to serialize Commonhaus user", e, null); + qc.addException(e); return null; } } diff --git a/cf-admin-bot/src/test/java/org/commonhaus/automation/admin/github/ContextHelper.java b/cf-admin-bot/src/test/java/org/commonhaus/automation/admin/github/ContextHelper.java index 5c23391..81e7ba9 100644 --- a/cf-admin-bot/src/test/java/org/commonhaus/automation/admin/github/ContextHelper.java +++ b/cf-admin-bot/src/test/java/org/commonhaus/automation/admin/github/ContextHelper.java @@ -18,9 +18,11 @@ import jakarta.json.JsonObject; import org.commonhaus.automation.admin.AdminDataCache; +import org.commonhaus.automation.admin.api.ApplicationData; import org.commonhaus.automation.admin.config.AdminConfigFile; import org.commonhaus.automation.github.context.ActionType; import org.commonhaus.automation.github.context.BaseQueryCache; +import org.commonhaus.automation.github.context.DataLabel; import org.commonhaus.automation.github.context.EventType; import org.commonhaus.automation.github.context.QueryContext; import org.commonhaus.automation.github.discovery.DiscoveryAction; @@ -53,6 +55,17 @@ public class ContextHelper extends QueryContext { public static final String botLogin = "commonhaus-bot"; public static final String botNodeId = "U_kgDOCVHtbA"; + public static final String datastoreRepoId = "R_kgDOL8tG0g"; + + public static final DataLabel APP_NEW = new DataLabel.Builder() + .name(ApplicationData.NEW).build(); + public static final DataLabel APP_ACCEPTED = new DataLabel.Builder() + .name(ApplicationData.ACCEPTED).build(); + public static final DataLabel APP_DECLINED = new DataLabel.Builder() + .id("LA_kwDOKRPTI88AAAABhGp_7g") + .name(ApplicationData.DECLINED).build(); + public static final Set APP_LABELS = Set.of(APP_NEW, APP_ACCEPTED, APP_DECLINED); + @Singleton static class AppObjectMapperCustomizer implements ObjectMapperCustomizer { public void customize(ObjectMapper mapper) { @@ -120,12 +133,14 @@ public GitHub setupMockTeam(GitHubMockSetupContext mocks) throws IOException { return gh; } - protected GHRepository setupMockRepository(GitHubMockSetupContext mocks, GitHub gh, AppContextService ctx, String repoName) + protected GHRepository setupMockRepository(GitHubMockSetupContext mocks, GitHub gh, AppContextService ctx, + String repoName) throws IOException { return setupMockRepository(mocks, gh, ctx, repoName, "R_" + repoName.hashCode()); } - protected GHRepository setupMockRepository(GitHubMockSetupContext mocks, GitHub gh, AppContextService ctx, String repoName, + protected GHRepository setupMockRepository(GitHubMockSetupContext mocks, GitHub gh, AppContextService ctx, + String repoName, String nodeId) throws IOException { GHRepository repo = mocks.repository(repoName); @@ -173,10 +188,11 @@ public GitHub setupBotGithub(AppContextService ctx, GitHubMockSetupContext mocks when(gh.getUser(botLogin)).thenReturn(bot); AdminDataCache.USER_CONNECTION.put(botNodeId, gh); - GHRepository dataStoreRepo = setupMockRepository(mocks, gh, ctx, ctx.getDataStore(), "R_kgDOL8tG0g"); + GHRepository dataStoreRepo = setupMockRepository(mocks, gh, ctx, ctx.getDataStore(), datastoreRepoId); RepositoryDiscoveryEvent repoEvent = new RepositoryDiscoveryEvent( DiscoveryAction.ADDED, gh, dql, installationId, dataStoreRepo, Optional.ofNullable(null)); + BaseQueryCache.LABELS.computeIfAbsent(datastoreRepoId, (k) -> new HashSet<>()).addAll(APP_LABELS); ctx.repositoryDiscovered(repoEvent); ctx.attestationIds.add("member"); diff --git a/cf-admin-bot/src/test/java/org/commonhaus/automation/admin/github/TeamMemberSyncTest.java b/cf-admin-bot/src/test/java/org/commonhaus/automation/admin/github/TeamMemberSyncTest.java index b25e5e1..bf2f848 100644 --- a/cf-admin-bot/src/test/java/org/commonhaus/automation/admin/github/TeamMemberSyncTest.java +++ b/cf-admin-bot/src/test/java/org/commonhaus/automation/admin/github/TeamMemberSyncTest.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -19,6 +20,7 @@ import jakarta.inject.Inject; import org.commonhaus.automation.admin.config.AdminConfigFile; +import org.commonhaus.automation.github.context.BaseQueryCache; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.kohsuke.github.GHContent; @@ -46,6 +48,7 @@ public class TeamMemberSyncTest extends ContextHelper { @BeforeEach void init() { mailbox.clear(); + BaseQueryCache.LABELS.computeIfAbsent(datastoreRepoId, (k) -> new HashSet<>()).addAll(APP_LABELS); } @Test