Skip to content

Commit 80faf59

Browse files
committed
Implement using reactions for confirmation in most cases
1 parent cb20e74 commit 80faf59

2 files changed

Lines changed: 163 additions & 66 deletions

File tree

src/Logic.hs

Lines changed: 95 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,79 +1152,116 @@ proceedUntilFixedPoint state = do
11521152
then return state
11531153
else proceedUntilFixedPoint newState
11541154

1155-
-- Describe the status of the pull request.
1156-
describeStatus :: BaseBranch -> PullRequestId -> PullRequest -> ProjectState -> Text
1157-
describeStatus (BaseBranch projectBaseBranchName) prId pr state = case Pr.classifyPullRequest pr of
1158-
PrStatusAwaitingApproval -> "Pull request awaiting approval."
1155+
-- | Feedback on a successfully parsed command.
1156+
data Feedback
1157+
= -- | Leave a comment.
1158+
CommentFeedback Text
1159+
| -- | Leave only a reaction.
1160+
ReactionFeedback ReactableId GithubApi.ReactionContent
1161+
1162+
-- | Determine what kind of feedback to leave based on the status of a PR.
1163+
feedbackOnStatus :: BaseBranch -> PullRequestId -> PullRequest -> ProjectState -> Feedback
1164+
feedbackOnStatus (BaseBranch projectBaseBranchName) prId pr state = case Pr.classifyPullRequest pr of
1165+
PrStatusAwaitingApproval -> CommentFeedback "Pull request awaiting approval."
11591166
PrStatusApproved ->
11601167
let
1161-
Approval (Username approvedBy) _source approvalType _position retriedBy priority = fromJust $ Pr.approval pr
1168+
Approval (Username approvedBy) source approvalType _position retriedBy priority = fromJust $ Pr.approval pr
11621169

11631170
approvalCommand = Pr.displayMergeCommand (Approve approvalType)
11641171
retriedByMsg = case retriedBy of
11651172
Just user -> format " (retried by @{})" [user]
11661173
Nothing -> mempty
1167-
queuePositionMsg = case Pr.getQueuePosition prId state of
1174+
queuePosition = Pr.getQueuePosition prId state
1175+
queuePositionMsg = case queuePosition of
11681176
0 -> "rebasing now"
11691177
1 -> "waiting for rebase behind one pull request"
11701178
n -> format "waiting for rebase behind {} pull requests" [n]
11711179
priorityMsg = case priority of
11721180
Normal -> mempty
11731181
High -> " with high priority"
1174-
in format "Pull request approved for {}{} by @{}{}, {}." [approvalCommand, priorityMsg, approvedBy, retriedByMsg, queuePositionMsg]
1182+
in
1183+
case (queuePosition, source) of
1184+
(0, Just reactable) -> ReactionFeedback reactable GithubApi.PlusOne
1185+
_ ->
1186+
CommentFeedback $
1187+
format
1188+
"Pull request approved for {}{} by @{}{}, {}."
1189+
[approvalCommand, priorityMsg, approvedBy, retriedByMsg, queuePositionMsg]
11751190
PrStatusOutdated ->
11761191
let BaseBranch baseBranchName = Pr.baseBranch pr
1177-
in format "Push to {} detected, rebasing again." [baseBranchName]
1178-
PrStatusBuildPending -> let Sha sha = fromJust $ Pr.integrationSha pr
1179-
train = takeWhile (/= prId) $ Pr.unfailedIntegratedPullRequests state
1180-
len = length train
1181-
prs = if len == 1 then "PR" else "PRs"
1182-
in case train of
1183-
[] -> Text.concat ["Rebased as ", sha, ", waiting for CI …"]
1184-
(_:_) -> Text.concat [ "Speculatively rebased as ", sha
1185-
, " behind ", Text.pack $ show len
1186-
, " other ", prs
1187-
, ", waiting for CI …"
1188-
]
1189-
PrStatusBuildStarted url -> Text.concat ["[CI job :yellow_circle:](", url, ") started."]
1190-
PrStatusAwaitingPromotion -> "The PR is waiting to be pushed to the target branch"
1191-
PrStatusIntegrated -> "The build succeeded."
1192+
in CommentFeedback $ format "Push to {} detected, rebasing again." [baseBranchName]
1193+
PrStatusBuildPending ->
1194+
let Sha sha = fromJust $ Pr.integrationSha pr
1195+
train = takeWhile (/= prId) $ Pr.unfailedIntegratedPullRequests state
1196+
len = length train
1197+
prs = if len == 1 then "PR" else "PRs"
1198+
in CommentFeedback $ case train of
1199+
[] -> Text.concat ["Rebased as ", sha, ", waiting for CI …"]
1200+
(_ : _) ->
1201+
Text.concat
1202+
[ "Speculatively rebased as "
1203+
, sha
1204+
, " behind "
1205+
, Text.pack $ show len
1206+
, " other "
1207+
, prs
1208+
, ", waiting for CI …"
1209+
]
1210+
PrStatusBuildStarted url -> CommentFeedback $ Text.concat ["[CI job :yellow_circle:](", url, ") started."]
1211+
PrStatusAwaitingPromotion -> CommentFeedback "The PR is waiting to be pushed to the target branch"
1212+
PrStatusIntegrated -> CommentFeedback "The build succeeded."
11921213
PrStatusIncorrectBaseBranch ->
11931214
let BaseBranch baseBranchName = Pr.baseBranch pr
1194-
in format "Merge rejected: the base branch of this pull request must be set to {}. It is currently set to {}."
1195-
[projectBaseBranchName, baseBranchName]
1196-
PrStatusWrongFixups -> "Pull request cannot be integrated as it contains fixup commits that do not belong to any other commits."
1197-
PrStatusEmptyRebase -> "Empty rebase. \
1198-
\ Have the changes already been merged into the target branch? \
1199-
\ Aborting."
1215+
in CommentFeedback $
1216+
format
1217+
"Merge rejected: the base branch of this pull request must be set to {}. It is currently set to {}."
1218+
[projectBaseBranchName, baseBranchName]
1219+
PrStatusWrongFixups ->
1220+
CommentFeedback "Pull request cannot be integrated as it contains fixup commits that do not belong to any other commits."
1221+
PrStatusEmptyRebase ->
1222+
CommentFeedback
1223+
"Empty rebase. \
1224+
\ Have the changes already been merged into the target branch? \
1225+
\ Aborting."
12001226
PrStatusFailedConflict ->
12011227
let
12021228
BaseBranch targetBranchName = Pr.baseBranch pr
12031229
Branch prBranchName = Pr.branch pr
1204-
in Text.concat
1205-
[ "Failed to rebase, please rebase manually using\n\n"
1206-
, " git fetch && git rebase --interactive --autosquash origin/"
1207-
, targetBranchName
1208-
, " "
1209-
, prBranchName
1210-
]
1230+
in
1231+
CommentFeedback $
1232+
Text.concat
1233+
[ "Failed to rebase, please rebase manually using\n\n"
1234+
, " git fetch && git rebase --interactive --autosquash origin/"
1235+
, targetBranchName
1236+
, " "
1237+
, prBranchName
1238+
]
12111239
-- The following is not actually shown to the user
12121240
-- as it is never set with needsFeedback=True,
12131241
-- but here in case we decide to show it.
1214-
PrStatusSpeculativeConflict -> "Failed to speculatively rebase. \
1215-
\ I will retry rebasing automatically when the queue clears."
1216-
PrStatusFailedBuild url -> case Pr.unfailedIntegratedPullRequestsBefore pr state of
1217-
-- On Fridays the retry command is also `retry on friday`. We currently
1218-
-- don't have that information here. Is that worth including?
1219-
[] -> format "The {}.\n\n\
1220-
\If this is the result of a flaky test, \
1221-
\then tag me again with the `retry` command. \
1222-
\Otherwise, push a new commit and tag me again."
1223-
[markdownLink "build failed :x:" url]
1224-
trainBefore -> format "Speculative {}. \
1225-
\ I will automatically retry after getting build results for {}."
1226-
[ markdownLink "build failed :x:" url
1227-
, prettyPullRequestIds trainBefore ]
1242+
PrStatusSpeculativeConflict ->
1243+
CommentFeedback
1244+
"Failed to speculatively rebase. \
1245+
\ I will retry rebasing automatically when the queue clears."
1246+
PrStatusFailedBuild url ->
1247+
CommentFeedback $
1248+
case Pr.unfailedIntegratedPullRequestsBefore pr state of
1249+
-- On Fridays the retry command is also `retry on friday`. We currently
1250+
-- don't have that information here. Is that worth including?
1251+
[] ->
1252+
format
1253+
"The {}.\n\n\
1254+
\If this is the result of a flaky test, \
1255+
\then tag me again with the `retry` command. \
1256+
\Otherwise, push a new commit and tag me again."
1257+
[markdownLink "build failed :x:" url]
1258+
trainBefore ->
1259+
format
1260+
"Speculative {}. \
1261+
\ I will automatically retry after getting build results for {}."
1262+
[ markdownLink "build failed :x:" url
1263+
, prettyPullRequestIds trainBefore
1264+
]
12281265

12291266
-- Leave a comment with the feedback from 'describeStatus' and set the
12301267
-- 'needsFeedback' flag to 'False'.
@@ -1235,11 +1272,15 @@ leaveFeedback
12351272
-> Eff es ProjectState
12361273
leaveFeedback (prId, pr) state = do
12371274
projectBaseBranch <- getBaseBranch
1238-
let message = describeStatus projectBaseBranch prId pr state
1239-
-- Hoff shouldn't reply to any of its own feedback messages. This can happen
1240-
-- if external automation causes the bot to issue a merge command to itself.
1241-
-- In that case the bot may tag itself when the merge gets approved.
1242-
() <- leaveComment prId $ hoffIgnoreComment <> message
1275+
case feedbackOnStatus projectBaseBranch prId pr state of
1276+
CommentFeedback message ->
1277+
-- Hoff shouldn't reply to any of its own feedback messages. This can happen
1278+
-- if external automation causes the bot to issue a merge command to itself.
1279+
-- In that case the bot may tag itself when the merge gets approved.
1280+
leaveComment prId $ hoffIgnoreComment <> message
1281+
ReactionFeedback reactable reaction ->
1282+
addReaction reactable reaction
1283+
12431284
pure $ Pr.setNeedsFeedback prId False state
12441285

12451286
-- Run 'leaveFeedback' on all pull requests that need feedback.

tests/Spec.hs

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
import Data.Aeson (decode, encode, eitherDecode)
1919
import Data.ByteString.Lazy (readFile)
2020
import Data.Either (isRight)
21-
import Data.Foldable (foldlM)
21+
import Data.Foldable (foldlM, for_)
22+
import Data.Function ((&))
2223
import Data.IntSet (IntSet)
2324
import Data.List (group)
2425
import Data.Maybe (fromJust, isNothing)
@@ -767,7 +768,8 @@ main = hspec $ do
767768

768769
it "handles merge command in body of pull request" $ do
769770
let
770-
event = PullRequestOpened (PullRequestId 1) (Branch "p") masterBranch (Sha "e0f") "title" "deckard" (Just "@bot merge")
771+
prId = PullRequestId 1
772+
event = PullRequestOpened prId (Branch "p") masterBranch (Sha "e0f") "title" "deckard" (Just "@bot merge")
771773
-- For this test, we assume all integrations and pushes succeed.
772774
results = defaultResults
773775
{ resultIntegrate = [Right (Sha "b71")] }
@@ -776,10 +778,10 @@ main = hspec $ do
776778

777779
actions `shouldBe`
778780
[ AIsReviewer "deckard"
779-
, ALeaveComment (PullRequestId 1) "<!-- Hoff: ignore -->\nPull request approved for merge by @deckard, rebasing now."
781+
, AAddReaction (OnPullRequest prId) GithubApi.PlusOne
780782
, ATryIntegrate "Merge #1: title\n\nApproved-by: deckard\nPriority: Normal\nAuto-deploy: false\n"
781-
(PullRequestId 1, Branch "refs/pull/1/head", Sha "e0f") [] False
782-
, ALeaveComment (PullRequestId 1) "<!-- Hoff: ignore -->\nRebased as b71, waiting for CI …"
783+
(prId, Branch "refs/pull/1/head", Sha "e0f") [] False
784+
, ALeaveComment prId "<!-- Hoff: ignore -->\nRebased as b71, waiting for CI …"
783785
]
784786
classifiedPullRequestIds state' `shouldBe` ClassifiedPullRequestIds
785787
{ building = [PullRequestId 1]
@@ -790,10 +792,11 @@ main = hspec $ do
790792

791793
it "does not handle merge command in body of reopened pull request" $ do
792794
let
795+
prId = PullRequestId 1
793796
events =
794-
[ PullRequestOpened (PullRequestId 1) (Branch "p") masterBranch (Sha "e0f") "title" "deckard" (Just "@bot merge")
795-
, PullRequestClosed (PullRequestId 1)
796-
, PullRequestOpened (PullRequestId 1) (Branch "p") masterBranch (Sha "e0f") "title" "deckard" Nothing
797+
[ PullRequestOpened prId (Branch "p") masterBranch (Sha "e0f") "title" "deckard" (Just "@bot merge")
798+
, PullRequestClosed prId
799+
, PullRequestOpened prId (Branch "p") masterBranch (Sha "e0f") "title" "deckard" Nothing
797800
]
798801
-- For this test, we assume all integrations and pushes succeed.
799802
results = defaultResults
@@ -803,11 +806,11 @@ main = hspec $ do
803806

804807
actions `shouldBe`
805808
[ AIsReviewer "deckard"
806-
, ALeaveComment (PullRequestId 1) "<!-- Hoff: ignore -->\nPull request approved for merge by @deckard, rebasing now."
809+
, AAddReaction (OnPullRequest prId) GithubApi.PlusOne
807810
, ATryIntegrate "Merge #1: title\n\nApproved-by: deckard\nPriority: Normal\nAuto-deploy: false\n"
808-
(PullRequestId 1, Branch "refs/pull/1/head", Sha "e0f") [] False
809-
, ALeaveComment (PullRequestId 1) "<!-- Hoff: ignore -->\nRebased as b71, waiting for CI …"
810-
, ALeaveComment (PullRequestId 1) "Abandoning this pull request because it was closed."
811+
(prId, Branch "refs/pull/1/head", Sha "e0f") [] False
812+
, ALeaveComment prId "<!-- Hoff: ignore -->\nRebased as b71, waiting for CI …"
813+
, ALeaveComment prId "Abandoning this pull request because it was closed."
811814
, ACleanupTestBranch (PullRequestId 1)
812815
]
813816
classifiedPullRequestIds state' `shouldBe` ClassifiedPullRequestIds
@@ -934,6 +937,59 @@ main = hspec $ do
934937
fromJust (Project.lookupPullRequest prId state') `shouldSatisfy`
935938
(\pr -> (Project.approval pr >>= Project.approvalSource) == Just (OnPullRequest prId))
936939

940+
it "adds a reaction to a 'merge' command in the common case" $ do
941+
let
942+
prId = PullRequestId 1
943+
commentId = CommentId 42
944+
state = singlePullRequestState prId (Branch "p") masterBranch (Sha "abc1234") "tyrell"
945+
946+
event = CommentAdded prId "deckard" (Just commentId) "@bot merge"
947+
948+
results = defaultResults { resultIntegrate = [Right (Sha "def2345")] }
949+
(_state', actions) = runActionCustom results $ handleEventTest event state
950+
951+
actions `shouldContain` [AAddReaction (OnIssueComment commentId) GithubApi.PlusOne]
952+
953+
it "adds a reaction to a 'retry' command in the common case" $ do
954+
let
955+
prId = PullRequestId 1
956+
mergeCommentId = CommentId 42
957+
retryCommentId = CommentId 72
958+
state = singlePullRequestState prId (Branch "p") masterBranch (Sha "1b1") "tyrell"
959+
960+
events =
961+
[ CommentAdded prId "deckard" (Just mergeCommentId) "@bot merge"
962+
, BuildStatusChanged (Sha "1b3") "default" (Project.BuildFailed (Just "url"))
963+
, CommentAdded prId "deckard" (Just retryCommentId) "@bot retry"
964+
]
965+
966+
results = defaultResults { resultIntegrate = [Right (Sha "1b3"), Right (Sha "00f")] }
967+
(_state', actions) = runActionCustom results $ handleEventsTest events state
968+
969+
actions `shouldContain` [AAddReaction (OnIssueComment retryCommentId) GithubApi.PlusOne]
970+
971+
it "falls back to an ordinary comment if there are other PRs ahead in the queue" $ do
972+
let
973+
(prId1, prId2) = (PullRequestId 1, PullRequestId 2)
974+
(mergeCommentId1, mergeCommentId2) = (CommentId 111, CommentId 222)
975+
state =
976+
Project.emptyProjectState
977+
& Project.insertPullRequest (PullRequestId 1) (Branch "one") masterBranch (Sha "111") "First PR" (Username "person")
978+
& Project.insertPullRequest (PullRequestId 2) (Branch "two") masterBranch (Sha "222") "Second PR" (Username "robot")
979+
events =
980+
[ CommentAdded prId1 "deckard" (Just mergeCommentId1) "@bot merge"
981+
, CommentAdded prId2 "deckard" (Just mergeCommentId2) "@bot merge"
982+
]
983+
results = defaultResults { resultIntegrate = [Right (Sha "11f"), Right (Sha "22f")] }
984+
(_state', actions) = runActionCustom results $ handleEventsTest events state
985+
986+
actions `shouldContain` [ALeaveComment prId2 "<!-- Hoff: ignore -->\nPull request approved for merge by @deckard, waiting for rebase behind one pull request."]
987+
988+
-- We check that we don't add /any/ reaction, not just that we don't add :+1:.
989+
let allPossibleReactions = [minBound .. maxBound]
990+
for_ allPossibleReactions $ \reaction ->
991+
actions `shouldNotContain` [AAddReaction (OnIssueComment mergeCommentId2) reaction]
992+
937993
it "recognizes 'merge and deploy' commands as the proper ApprovedFor value" $ do
938994
let
939995
prId = PullRequestId 1

0 commit comments

Comments
 (0)