fix(agentic): release issue lock on dispatch failure
This commit is contained in:
parent
1873adb6ae
commit
a3c39ccae7
2 changed files with 109 additions and 0 deletions
|
|
@ -78,6 +78,27 @@ func (s *PrepSubsystem) dispatchIssue(ctx context.Context, req *mcp.CallToolRequ
|
|||
if err := s.lockIssue(ctx, input.Org, input.Repo, input.Issue, input.Agent); err != nil {
|
||||
return nil, DispatchOutput{}, err
|
||||
}
|
||||
|
||||
var dispatchErr error
|
||||
defer func() {
|
||||
if dispatchErr != nil {
|
||||
_ = s.unlockIssue(ctx, input.Org, input.Repo, input.Issue)
|
||||
}
|
||||
}()
|
||||
|
||||
result, out, dispatchErr := s.dispatch(ctx, req, DispatchInput{
|
||||
Repo: input.Repo,
|
||||
Org: input.Org,
|
||||
Issue: input.Issue,
|
||||
Task: issue.Title,
|
||||
Agent: input.Agent,
|
||||
Template: input.Template,
|
||||
DryRun: input.DryRun,
|
||||
})
|
||||
if dispatchErr != nil {
|
||||
return nil, DispatchOutput{}, dispatchErr
|
||||
}
|
||||
return result, out, nil
|
||||
}
|
||||
|
||||
return s.dispatch(ctx, req, DispatchInput{
|
||||
|
|
@ -91,6 +112,34 @@ func (s *PrepSubsystem) dispatchIssue(ctx context.Context, req *mcp.CallToolRequ
|
|||
})
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) unlockIssue(ctx context.Context, org, repo string, issue int) error {
|
||||
updateURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue)
|
||||
payload, err := json.Marshal(map[string]any{
|
||||
"assignees": []string{},
|
||||
})
|
||||
if err != nil {
|
||||
return coreerr.E("unlockIssue", "failed to encode issue unlock", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, updateURL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return coreerr.E("unlockIssue", "failed to build unlock request", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "token "+s.forgeToken)
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return coreerr.E("unlockIssue", "failed to update issue", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
return coreerr.E("unlockIssue", fmt.Sprintf("issue unlock returned %d", resp.StatusCode), nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) fetchIssue(ctx context.Context, org, repo string, issue int) (*forgeIssue, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
|
|
|
|||
|
|
@ -90,6 +90,66 @@ func TestDispatchIssue_Bad_AssignedIssue(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDispatchIssue_Good_UnlocksOnPrepFailure(t *testing.T) {
|
||||
var methods []string
|
||||
var bodies []string
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
methods = append(methods, r.Method)
|
||||
bodies = append(bodies, string(body))
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"title": "Fix login crash",
|
||||
"body": "details",
|
||||
"state": "open",
|
||||
})
|
||||
case http.MethodPatch:
|
||||
w.WriteHeader(http.StatusOK)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
s := &PrepSubsystem{
|
||||
forgeURL: srv.URL,
|
||||
forgeToken: "token",
|
||||
client: srv.Client(),
|
||||
codePath: t.TempDir(),
|
||||
}
|
||||
|
||||
_, _, err := s.dispatchIssue(context.Background(), nil, IssueDispatchInput{
|
||||
Repo: "demo",
|
||||
Org: "core",
|
||||
Issue: 42,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected dispatch to fail when the repo clone is missing")
|
||||
}
|
||||
|
||||
if got, want := len(methods), 3; got != want {
|
||||
t.Fatalf("expected %d requests, got %d (%v)", want, got, methods)
|
||||
}
|
||||
if methods[0] != http.MethodGet {
|
||||
t.Fatalf("expected first request to fetch issue, got %s", methods[0])
|
||||
}
|
||||
if methods[1] != http.MethodPatch {
|
||||
t.Fatalf("expected second request to lock issue, got %s", methods[1])
|
||||
}
|
||||
if methods[2] != http.MethodPatch {
|
||||
t.Fatalf("expected third request to unlock issue, got %s", methods[2])
|
||||
}
|
||||
if !strings.Contains(bodies[1], `"assignees":["claude"]`) {
|
||||
t.Fatalf("expected lock request to assign claude, got %s", bodies[1])
|
||||
}
|
||||
if !strings.Contains(bodies[2], `"assignees":[]`) {
|
||||
t.Fatalf("expected unlock request to clear assignees, got %s", bodies[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockIssue_Good_RequestBody(t *testing.T) {
|
||||
var gotMethod string
|
||||
var gotPath string
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue