Skip to content

Commit acb37ae

Browse files
πŸ› contribute: keep a PR-covered contributor claim on lease revoke (#17305)
* πŸ› contribute: convert a PR-covered contributor claim instead of releasing it on lease revoke Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> * πŸ“– api-reference: refresh claims_api.go citations after lease-revoke change Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> * πŸ› contribute: hold the claim from the verified task_complete PR, not the scan ledger Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> --------- Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> Co-authored-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c4d89da commit acb37ae

8 files changed

Lines changed: 329 additions & 11 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- contribute: when a relay contributor's lease ends while their own open PR covers the issue, the claim is now kept as a PR-held claim by the same contributor until that PR merges or closes, instead of being released with `lease revoked` and leaving the issue looking unclaimed to dispatch, the PR-request gate and the claim listing ([#17300](https://github.com/hivecommons/hive/issues/17300)).

β€Žsrc/docs/api-reference/contribute.mdβ€Ž

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ Part of the [Dashboard REST API reference](../api-reference.md); see that page f
6767

6868
| Method | Path | Auth | Purpose | Source |
6969
|---|---|---|---|---|
70-
| `GET` | `/api/claims` | Dashboard auth | Every live worker claim (`{enabled, claims:[…]}`); `enabled:false` when `claims.enabled: false` | `pkg/dashboard/claims_api.go:53` |
71-
| `GET` | `/api/claims/{owner}/{repo}/{number}` | Dashboard auth | `{held, claim}` for one issue | `pkg/dashboard/claims_api.go:54` |
72-
| `POST` | `/api/claims/{owner}/{repo}/{number}` | Signed-in / hub-proxied / GitHub-token caller claims as a **human** under their login; a verified owner token claims as the owner | Body `{force, ttl_s, session}`. 200 with `outcome` `claimed`/`renewed`/`taken_over`; 409 with `outcome` `held` (same rank, retry with `force`) or `refused` (higher rank holds it) and a `hint` | `pkg/dashboard/claims_api.go:55` |
73-
| `DELETE` | `/api/claims/{owner}/{repo}/{number}` | Same identity rules | Body `{force, reason}`. Releases your claim or a lower-ranked holder's; `force` with a verified owner role releases regardless of holder | `pkg/dashboard/claims_api.go:56` |
70+
| `GET` | `/api/claims` | Dashboard auth | Every live worker claim (`{enabled, claims:[…]}`); `enabled:false` when `claims.enabled: false` | `pkg/dashboard/claims_api.go:54` |
71+
| `GET` | `/api/claims/{owner}/{repo}/{number}` | Dashboard auth | `{held, claim}` for one issue | `pkg/dashboard/claims_api.go:55` |
72+
| `POST` | `/api/claims/{owner}/{repo}/{number}` | Signed-in / hub-proxied / GitHub-token caller claims as a **human** under their login; a verified owner token claims as the owner | Body `{force, ttl_s, session}`. 200 with `outcome` `claimed`/`renewed`/`taken_over`; 409 with `outcome` `held` (same rank, retry with `force`) or `refused` (higher rank holds it) and a `hint` | `pkg/dashboard/claims_api.go:56` |
73+
| `DELETE` | `/api/claims/{owner}/{repo}/{number}` | Same identity rules | Body `{force, reason}`. Releases your claim or a lower-ranked holder's; `force` with a verified owner role releases regardless of holder | `pkg/dashboard/claims_api.go:57` |
7474

7575
### `/api/v1` contributor subpaths
7676

β€Žsrc/pkg/claims/claims.goβ€Ž

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ type Claim struct {
114114
// stage/020-claimed whatever its Kind (hivecommons/hive#13069). Once set
115115
// it stays for the claim's life β€” a renewal never clears it.
116116
PRStarted bool `json:"pr_started,omitempty"`
117+
// PRRepo / PRNumber name the open pull request a claim is held for once
118+
// its holder's lease ended with that PR covering the issue
119+
// (hivecommons/hive#17300, HoldForPR). Zero for every other claim.
120+
PRRepo string `json:"pr_repo,omitempty"`
121+
PRNumber int `json:"pr_number,omitempty"`
117122
// ClaimedAt is when the current holder took the claim.
118123
ClaimedAt time.Time `json:"claimed_at"`
119124
// ExpiresAt is when the claim lapses unless renewed.
@@ -721,6 +726,48 @@ func (l *Ledger) ReleaseByHolderID(holderID, reason string, onlyKey ...string) [
721726
return out
722727
}
723728

729+
// HoldForPR keeps holderID's live claim on key as a PR-held claim instead of
730+
// letting it go with the lease that carried it (hivecommons/hive#17300): it
731+
// records the covering pull request (PRStarted, PRRepo, PRNumber) and pushes
732+
// the expiry out to the policy's longest hold, so every ledger reader keeps
733+
// seeing the issue as taken by the same principal while the PR is open. The
734+
// caller re-confirms the PR and calls it again to renew, or releases the
735+
// claim once the PR merges or closes. OnClaimed fires only when the PR
736+
// evidence changed, so a periodic renewal does not touch GitHub. Reports
737+
// false when holderID holds no live claim on key.
738+
func (l *Ledger) HoldForPR(holderID, key, prRepo string, prNumber int) (Claim, bool) {
739+
prRepo = strings.TrimSpace(prRepo)
740+
if l == nil || holderID == "" || key == "" || prNumber <= 0 {
741+
return Claim{}, false
742+
}
743+
l.mu.Lock()
744+
now := l.now()
745+
c, ok := l.claims[key]
746+
if !ok || c.Expired(now) || c.HolderID != holderID {
747+
l.mu.Unlock()
748+
return Claim{}, false
749+
}
750+
changed := !c.PRStarted || c.PRRepo != prRepo || c.PRNumber != prNumber
751+
c.PRStarted, c.PRRepo, c.PRNumber = true, prRepo, prNumber
752+
hold := l.policy.MaxTTL
753+
if hold <= 0 {
754+
hold = DefaultMaxTTL
755+
}
756+
c.ExpiresAt = now.Add(hold)
757+
l.claims[key] = c
758+
_ = l.saveLocked()
759+
hooks := l.hooks
760+
l.mu.Unlock()
761+
if changed && hooks.OnClaimed != nil {
762+
hooks.OnClaimed(c, OutcomeRenewed)
763+
}
764+
return c, true
765+
}
766+
767+
// ReasonCoveringPRDone is the release reason for a PR-held claim (HoldForPR)
768+
// whose covering pull request merged, closed, or stopped covering the issue.
769+
const ReasonCoveringPRDone = "covering PR merged or closed"
770+
724771
// ReasonHolderRelaunched is the release reason recorded when a hive agent's
725772
// CLI was relaunched (hive boot, operator or self-heal restart): the context
726773
// that held the claim is gone, so the issue goes back to the pool at once

β€Žsrc/pkg/claims/claims_test.goβ€Ž

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,3 +624,36 @@ func TestReleaseAgentClaimsDropsOnlyThatAgentsClaims(t *testing.T) {
624624
t.Fatal("ReleaseComment must stamp at=")
625625
}
626626
}
627+
628+
// A contributor claim whose holder's PR covers the issue is kept as a PR-held
629+
// claim rather than dropped with the lease (hivecommons/hive#17300).
630+
func TestHoldForPRKeepsClaimWithPREvidence(t *testing.T) {
631+
var claimed []Claim
632+
l, now := newTestLedger(t, Hooks{OnClaimed: func(c Claim, _ Outcome) { claimed = append(claimed, c) }})
633+
if _, err := l.Claim(Request{Repo: "o/r", Issue: 9, Holder: "karibhou", HolderID: "c-k#a", Kind: KindContributor}); err != nil {
634+
t.Fatal(err)
635+
}
636+
if _, ok := l.HoldForPR("someone-else", Key("o/r", 9), "o/r", 12); ok {
637+
t.Fatal("another holder must not convert the claim")
638+
}
639+
held, ok := l.HoldForPR("c-k#a", Key("o/r", 9), "o/r", 12)
640+
if !ok || !held.PRStarted || held.PRRepo != "o/r" || held.PRNumber != 12 || held.Holder != "karibhou" || held.Kind != KindContributor {
641+
t.Fatalf("HoldForPR = %+v ok=%v", held, ok)
642+
}
643+
if held.ExpiresAt != now.Add(DefaultMaxTTL) {
644+
t.Fatalf("expires=%v want +%v", held.ExpiresAt, DefaultMaxTTL)
645+
}
646+
got, ok := l.Lookup("o/r", 9)
647+
if !ok || got.HolderID != "c-k#a" || got.PRNumber != 12 || !got.PRStarted {
648+
t.Fatalf("ledger claim = %+v ok=%v", got, ok)
649+
}
650+
if len(claimed) != 2 || !claimed[1].PRStarted {
651+
t.Fatalf("OnClaimed must fire once for the conversion: %+v", claimed)
652+
}
653+
if _, ok := l.HoldForPR("c-k#a", Key("o/r", 9), "o/r", 12); !ok || len(claimed) != 2 {
654+
t.Fatalf("a renewal of the same PR hold must not re-fire OnClaimed: %d", len(claimed))
655+
}
656+
if _, ok := l.HoldForPR("c-k#a", Key("o/r", 10), "o/r", 12); ok {
657+
t.Fatal("no claim on the key must report false")
658+
}
659+
}

β€Žsrc/pkg/dashboard/claims_api.goβ€Ž

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/hivecommons/hive/pkg/claims"
13+
ghpkg "github.com/hivecommons/hive/pkg/github"
1314
"github.com/hivecommons/hive/pkg/hooks"
1415
"github.com/hivecommons/hive/pkg/worksource"
1516
)
@@ -178,7 +179,10 @@ func (h *ContributeWSHub) renewClaimForLease(identity, repo string, number int)
178179
}
179180

180181
// releaseClaimForLease drops the contributor claim that travelled with a lease
181-
// when the lease is revoked (task done, abandoned, yanked, or expired).
182+
// when the lease is revoked (task done, abandoned, yanked, or expired). When
183+
// the holder's own open PR already covers the issue the claim is converted to
184+
// a PR-held claim instead (hivecommons/hive#17300): the work is not abandoned,
185+
// and dropping the claim made every ledger-only reader see the issue as free.
182186
func (h *ContributeWSHub) releaseClaimForLease(identity, key, reason string) {
183187
l := h.claimsLedger()
184188
if l == nil || identity == "" {
@@ -187,11 +191,109 @@ func (h *ContributeWSHub) releaseClaimForLease(identity, key, reason string) {
187191
}
188192
return
189193
}
194+
if c, ok := l.LookupKey(key); ok && c.HolderID == identity && c.Kind == claims.KindContributor {
195+
if pr, covered := h.coveringPRByHolder(c); covered {
196+
if held, ok := l.HoldForPR(identity, key, coveringPRRepo(pr), pr.PRNumber); ok {
197+
h.logger.Info("[claims] contributor claim kept for covering PR", "issue", held.Key(), "holder", held.Holder,
198+
"pr", pr.PRNumber, "pr_url", pr.PRURL, "lease_reason", reason)
199+
return
200+
}
201+
}
202+
}
190203
for _, c := range l.ReleaseByHolderID(identity, reason, key) {
191204
h.logger.Info("[claims] contributor claim released with lease", "issue", c.Key(), "holder", c.Holder, "reason", reason)
192205
}
193206
}
194207

208+
// coveringPRByHolder reports the open pull request, authored by the claim's
209+
// holder, that the duplicate-PR claim ledger (Dependencies.IssueClaimed)
210+
// records as covering the claimed issue. Only a claim FilterClaimedIssues
211+
// would suppress the issue for counts (IssueClaim.CoversOpenIssue): a merged
212+
// or closed PR no longer holds anything. Both repo spellings are tried, as in
213+
// issueClaimedByOpenPR.
214+
func (h *ContributeWSHub) coveringPRByHolder(c claims.Claim) (ghpkg.IssueClaim, bool) {
215+
if c.Holder == "" {
216+
return ghpkg.IssueClaim{}, false
217+
}
218+
name := c.Repo
219+
if i := strings.LastIndex(name, "/"); i >= 0 {
220+
name = name[i+1:]
221+
}
222+
pr, ok := h.issueClaimedByOpenPR(c.Repo, name, c.Issue)
223+
if !ok || !pr.CoversOpenIssue() || !strings.EqualFold(strings.TrimSpace(pr.PRAuthor), c.Holder) {
224+
return ghpkg.IssueClaim{}, false
225+
}
226+
return pr, true
227+
}
228+
229+
// coveringPRRepo is the repository the covering PR lives in.
230+
func coveringPRRepo(pr ghpkg.IssueClaim) string {
231+
if pr.PRRepo != "" {
232+
return pr.PRRepo
233+
}
234+
return pr.Repo
235+
}
236+
237+
// recordCompletionPRClaim writes the PR a task_complete reported into the
238+
// duplicate-PR ledger IssueClaimed reads, once verifyReportedPRDetail has
239+
// confirmed it exists on the assigned repo and is this contributor's
240+
// (hivecommons/hive#17300). The ledger is otherwise only refreshed by the
241+
// eval-cycle scan and by PRs the hive's own client opens, so a PR the relay
242+
// opened with its own token minutes ago is usually missing from it β€” and the
243+
// lease revoke that follows the completion would then release the claim the
244+
// PR covers. Merged PRs are skipped: they hold nothing.
245+
func (h *ContributeWSHub) recordCompletionPRClaim(task *WSTaskAssign, prURL, username string, pr ghpkg.PRVerification) {
246+
if task == nil || !pr.Verified || pr.Merged || task.Kind != "issue" || task.Number <= 0 || username == "" {
247+
return
248+
}
249+
if task.SourceType != "" && task.SourceType != "github" {
250+
return
251+
}
252+
if h.server == nil || h.server.deps == nil || h.server.deps.RecordIssueClaim == nil {
253+
return
254+
}
255+
ref, err := ghpkg.ParsePRURL(prURL)
256+
if err != nil || !strings.EqualFold(ref.FullName(), task.Repo) {
257+
return
258+
}
259+
claim := ghpkg.IssueClaim{Repo: task.Repo, Issue: task.Number, PRRepo: ref.FullName(), PRNumber: ref.Number,
260+
PRURL: prURL, PRAuthor: username, PRState: ghpkg.PRStateOpen, ExternalAuthor: true}
261+
if err := h.server.deps.RecordIssueClaim(claim); err != nil {
262+
h.logger.Warn("[claims] recording completion PR claim failed",
263+
"repo", task.Repo, "issue", task.Number, "pr_url", prURL, "error", err.Error())
264+
}
265+
}
266+
267+
// sweepPRHeldClaims re-checks every contributor claim kept for a covering PR
268+
// (releaseClaimForLease, hivecommons/hive#17300): while the same PR still
269+
// covers the issue the hold is renewed; once it merged, closed, or stopped
270+
// covering the issue the claim is released. Without the duplicate-PR ledger
271+
// wired there is nothing to re-check against, so the holds are left to their
272+
// own TTL. Returns how many claims were released.
273+
func (h *ContributeWSHub) sweepPRHeldClaims() int {
274+
l := h.claimsLedger()
275+
if l == nil || h.server == nil || h.server.deps == nil || h.server.deps.IssueClaimed == nil {
276+
return 0
277+
}
278+
released := 0
279+
for _, c := range l.List() {
280+
if c.Kind != claims.KindContributor || c.PRNumber <= 0 || c.HolderID == "" {
281+
continue
282+
}
283+
// Any open PR by the holder that covers the issue keeps the hold, so
284+
// closing PR A for a replacement PR B re-holds the claim for B.
285+
if pr, ok := h.coveringPRByHolder(c); ok {
286+
l.HoldForPR(c.HolderID, c.Key(), coveringPRRepo(pr), pr.PRNumber)
287+
continue
288+
}
289+
for _, r := range l.ReleaseByHolderID(c.HolderID, claims.ReasonCoveringPRDone, c.Key()) {
290+
released++
291+
h.logger.Info("[claims] PR-held contributor claim released", "issue", r.Key(), "holder", r.Holder, "pr", r.PRNumber)
292+
}
293+
}
294+
return released
295+
}
296+
195297
// removeLegacyClaimLabelForLease tells the stage engine the contributor
196298
// claim that travelled with a lease is gone (a ClaimReleased fact, ADR-0025)
197299
// so the issue's stage/020-claimed comes off; the engine re-reads the issue

β€Žsrc/pkg/dashboard/claims_api_test.goβ€Ž

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"net/http/httptest"
88
"path/filepath"
9+
"strconv"
910
"strings"
1011
"testing"
1112
"time"
@@ -277,3 +278,128 @@ func TestClaims_RenewLeaseRenewsClaim(t *testing.T) {
277278
t.Fatalf("claim not renewed with lease: before=%v after=%+v", first.ExpiresAt, after)
278279
}
279280
}
281+
282+
// A lease that ends while the holder's own open PR covers the issue keeps the
283+
// claim as a PR-held claim for the same principal; once that PR stops
284+
// covering the issue the sweep releases it (hivecommons/hive#17300).
285+
func TestClaims_LeaseRevokeKeepsClaimCoveredByHoldersPR(t *testing.T) {
286+
hub, s, l := claimsHub(t)
287+
const identity = "c-karibhou#a"
288+
covering := ghpkg.IssueClaim{Repo: "myorg/repo1", Issue: 1, PRRepo: "myorg/repo1", PRNumber: 5,
289+
PRURL: "https://github.com/myorg/repo1/pull/5", PRAuthor: "Karibhou", ExternalAuthor: true, Closing: true}
290+
open := true
291+
s.deps.IssueClaimed = func(repo string, number int) (ghpkg.IssueClaim, bool) {
292+
if repo == "myorg/repo1" && number == 1 && open {
293+
return covering, true
294+
}
295+
return ghpkg.IssueClaim{}, false
296+
}
297+
if err := hub.recordLease(identity, "t-1", "myorg/repo1", 1, "contributor", 3, time.Now()); err != nil {
298+
t.Fatal(err)
299+
}
300+
if _, err := l.Claim(claims.Request{Repo: "myorg/repo1", Issue: 1, Holder: "karibhou", HolderID: identity, Kind: claims.KindContributor, Session: "a"}); err != nil {
301+
t.Fatal(err)
302+
}
303+
304+
hub.revokeLease(identity, "t-1")
305+
c, ok := l.Lookup("myorg/repo1", 1)
306+
if !ok || c.HolderID != identity || c.Holder != "karibhou" || !c.PRStarted || c.PRRepo != "myorg/repo1" || c.PRNumber != 5 {
307+
t.Fatalf("claim after revoke = %+v ok=%v, want PR-held claim by the same contributor", c, ok)
308+
}
309+
if hub.sweepPRHeldClaims() != 0 {
310+
t.Fatal("sweep released a claim whose PR still covers the issue")
311+
}
312+
if _, ok := l.Lookup("myorg/repo1", 1); !ok {
313+
t.Fatal("PR-held claim lost while the PR is open")
314+
}
315+
316+
open = false
317+
if n := hub.sweepPRHeldClaims(); n != 1 {
318+
t.Fatalf("sweep released %d claims once the PR closed, want 1", n)
319+
}
320+
if _, ok := l.Lookup("myorg/repo1", 1); ok {
321+
t.Fatal("PR-held claim survived its PR closing")
322+
}
323+
}
324+
325+
// Without a covering PR by the holder β€” none at all, or one by somebody
326+
// else β€” a revoked lease still releases the claim.
327+
func TestClaims_LeaseRevokeWithoutHoldersPRReleasesClaim(t *testing.T) {
328+
hub, s, l := claimsHub(t)
329+
const identity = "c-worker#a"
330+
s.deps.IssueClaimed = func(repo string, number int) (ghpkg.IssueClaim, bool) {
331+
if repo == "myorg/repo1" && number == 2 {
332+
return ghpkg.IssueClaim{Repo: repo, Issue: number, PRNumber: 8, PRAuthor: "someone-else", ExternalAuthor: true}, true
333+
}
334+
return ghpkg.IssueClaim{}, false
335+
}
336+
for n, task := range map[int]string{1: "t-1", 2: "t-2"} {
337+
if err := hub.recordLease(identity, task, "myorg/repo1", n, "contributor", 3, time.Now()); err != nil {
338+
t.Fatal(err)
339+
}
340+
if _, err := l.Claim(claims.Request{Repo: "myorg/repo1", Issue: n, Holder: "worker", HolderID: identity, Kind: claims.KindContributor, Session: "a"}); err != nil {
341+
t.Fatal(err)
342+
}
343+
hub.revokeLease(identity, task)
344+
if c, ok := l.Lookup("myorg/repo1", n); ok {
345+
t.Fatalf("issue %d: claim survived an abandon with no covering PR by its holder: %+v", n, c)
346+
}
347+
}
348+
}
349+
350+
// The primary #17300 path: the relay opened its PR with its own token and
351+
// reports task_complete before any scan saw it, so the duplicate-PR ledger is
352+
// empty at revoke time. The verified completion PR is recorded first and the
353+
// claim is held for it, not released with the lease.
354+
func TestClaims_CompletionPRHoldsClaimBeforeScanSeesIt(t *testing.T) {
355+
hub, s, l := claimsHub(t)
356+
const identity = "c-karibhou#a"
357+
ledger := map[string]ghpkg.IssueClaim{}
358+
s.deps.IssueClaimed = func(repo string, number int) (ghpkg.IssueClaim, bool) {
359+
c, ok := ledger[repo+"#"+strconv.Itoa(number)]
360+
return c, ok
361+
}
362+
s.deps.RecordIssueClaim = func(c ghpkg.IssueClaim) error {
363+
ledger[c.Repo+"#"+strconv.Itoa(c.Issue)] = c
364+
return nil
365+
}
366+
if err := hub.recordLease(identity, "t-1", "myorg/repo1", 1, "contributor", 3, time.Now()); err != nil {
367+
t.Fatal(err)
368+
}
369+
if _, err := l.Claim(claims.Request{Repo: "myorg/repo1", Issue: 1, Holder: "karibhou", HolderID: identity, Kind: claims.KindContributor, Session: "a"}); err != nil {
370+
t.Fatal(err)
371+
}
372+
if _, ok := s.deps.IssueClaimed("myorg/repo1", 1); ok {
373+
t.Fatal("precondition: scan ledger must not know the PR yet")
374+
}
375+
376+
task := &WSTaskAssign{TaskID: "t-1", Kind: "issue", Repo: "myorg/repo1", Number: 1}
377+
hub.recordCompletionPRClaim(task, "https://github.com/myorg/repo1/pull/9", "Karibhou", ghpkg.PRVerification{Verified: true})
378+
hub.revokeLease(identity, "t-1")
379+
380+
c, ok := l.Lookup("myorg/repo1", 1)
381+
if !ok || c.HolderID != identity || !c.PRStarted || c.PRRepo != "myorg/repo1" || c.PRNumber != 9 {
382+
t.Fatalf("claim after completion = %+v ok=%v, want held for the verified PR #9", c, ok)
383+
}
384+
if hub.sweepPRHeldClaims() != 0 {
385+
t.Fatal("sweep released a claim held for the verified completion PR")
386+
}
387+
388+
// An unverified or merged completion PR records nothing, so the claim
389+
// goes with the lease as before.
390+
for i, pr := range []ghpkg.PRVerification{{}, {Verified: true, Merged: true}} {
391+
n := 2 + i
392+
task := &WSTaskAssign{TaskID: "t-x", Kind: "issue", Repo: "myorg/repo1", Number: n}
393+
if err := hub.recordLease(identity, "t-x", "myorg/repo1", n, "contributor", 3, time.Now()); err != nil {
394+
t.Fatal(err)
395+
}
396+
if _, err := l.Claim(claims.Request{Repo: "myorg/repo1", Issue: n, Holder: "karibhou", HolderID: identity, Kind: claims.KindContributor, Session: "a"}); err != nil {
397+
t.Fatal(err)
398+
}
399+
hub.recordCompletionPRClaim(task, "https://github.com/myorg/repo1/pull/10", "Karibhou", pr)
400+
hub.revokeLease(identity, "t-x")
401+
if c, ok := l.Lookup("myorg/repo1", n); ok {
402+
t.Fatalf("issue %d: claim kept for an unverified/merged completion PR: %+v", n, c)
403+
}
404+
}
405+
}

0 commit comments

Comments
Β (0)