diff --git a/WORKFLOW.claude.md.example b/WORKFLOW.claude.md.example index efc13ea..04901f7 100644 --- a/WORKFLOW.claude.md.example +++ b/WORKFLOW.claude.md.example @@ -85,5 +85,12 @@ This is retry attempt {{ attempt }}. Review your previous work and continue from 2. Implement the necessary changes 3. Write tests for your changes 4. Ensure all tests pass -5. Create a pull request with a clear description -6. Move the issue to "Human Review" when complete +5. Before opening the PR, post a comment on the Linear ticket summarizing what was implemented and any relevant notes. Use the Linear API with `$LINEAR_API_KEY`: + ```bash + curl -s -X POST https://api.linear.app/graphql \ + -H "Content-Type: application/json" \ + -H "Authorization: $LINEAR_API_KEY" \ + -d '{"query":"mutation($issueId:String!,$body:String!){commentCreate(input:{issueId:$issueId,body:$body}){success}}","variables":{"issueId":"{{ issue.id }}","body":""}}' + ``` +6. Create a pull request with a clear description +7. Move the issue to "Human Review" when complete diff --git a/WORKFLOW.md.example b/WORKFLOW.md.example index 94a6ea2..cc23517 100644 --- a/WORKFLOW.md.example +++ b/WORKFLOW.md.example @@ -74,5 +74,12 @@ This is retry attempt {{ attempt }}. Review your previous work and continue from 2. Implement the necessary changes 3. Write tests for your changes 4. Ensure all tests pass -5. Create a pull request with a clear description -6. Move the issue to "Human Review" when complete +5. Before opening the PR, post a comment on the Linear ticket summarizing what was implemented and any relevant notes. Use the Linear API with `$LINEAR_API_KEY`: + ```bash + curl -s -X POST https://api.linear.app/graphql \ + -H "Content-Type: application/json" \ + -H "Authorization: $LINEAR_API_KEY" \ + -d '{"query":"mutation($issueId:String!,$body:String!){commentCreate(input:{issueId:$issueId,body:$body}){success}}","variables":{"issueId":"{{ issue.id }}","body":""}}' + ``` +6. Create a pull request with a clear description +7. Move the issue to "Human Review" when complete diff --git a/internal/linear/client.go b/internal/linear/client.go index 215d38f..882c8c4 100644 --- a/internal/linear/client.go +++ b/internal/linear/client.go @@ -186,6 +186,43 @@ func (c *Client) FetchIssuesByStates(ctx context.Context, states []string) ([]mo return allIssues, nil } +// CreateComment posts a comment on a Linear issue. +func (c *Client) CreateComment(ctx context.Context, issueID, body string) error { + mutation := `mutation($issueId: String!, $body: String!) { + commentCreate(input: { issueId: $issueId, body: $body }) { + success + } + }` + + resp, err := c.doQuery(ctx, mutation, map[string]any{ + "issueId": issueID, + "body": body, + }) + if err != nil { + return fmt.Errorf("linear_api_request: create comment: %w", err) + } + + var result struct { + Data struct { + CommentCreate struct { + Success bool `json:"success"` + } `json:"commentCreate"` + } `json:"data"` + Errors []graphqlError `json:"errors"` + } + if err := json.Unmarshal(resp, &result); err != nil { + return fmt.Errorf("linear_unknown_payload: %w", err) + } + if len(result.Errors) > 0 { + return fmt.Errorf("linear_graphql_errors: %s", result.Errors[0].Message) + } + if !result.Data.CommentCreate.Success { + return fmt.Errorf("linear_comment_failed: commentCreate returned success=false") + } + + return nil +} + // ExecuteGraphQL runs a raw GraphQL query (for the linear_graphql tool extension). func (c *Client) ExecuteGraphQL(ctx context.Context, query string, variables map[string]any) (json.RawMessage, error) { return c.doQuery(ctx, query, variables) diff --git a/internal/linear/client_test.go b/internal/linear/client_test.go new file mode 100644 index 0000000..b68dbf8 --- /dev/null +++ b/internal/linear/client_test.go @@ -0,0 +1,104 @@ +package linear + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func newTestClient(t *testing.T, handler http.HandlerFunc) *Client { + t.Helper() + srv := httptest.NewServer(handler) + t.Cleanup(srv.Close) + return NewClient(srv.URL, "test-key", "test-project") +} + +func TestCreateComment_Success(t *testing.T) { + called := false + client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { + called = true + + var req map[string]any + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("decode request: %v", err) + } + + vars := req["variables"].(map[string]any) + if vars["issueId"] != "issue-123" { + t.Errorf("expected issueId=issue-123, got %v", vars["issueId"]) + } + if vars["body"] != "Implementation complete." { + t.Errorf("expected body='Implementation complete.', got %v", vars["body"]) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "commentCreate": map[string]any{ + "success": true, + }, + }, + }) + }) + + err := client.CreateComment(context.Background(), "issue-123", "Implementation complete.") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !called { + t.Fatal("handler was not called") + } +} + +func TestCreateComment_GraphQLError(t *testing.T) { + client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]any{ + {"message": "unauthorized"}, + }, + }) + }) + + err := client.CreateComment(context.Background(), "issue-123", "notes") + if err == nil { + t.Fatal("expected error, got nil") + } + if want := "linear_graphql_errors: unauthorized"; err.Error() != want { + t.Errorf("expected %q, got %q", want, err.Error()) + } +} + +func TestCreateComment_SuccessFalse(t *testing.T) { + client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "commentCreate": map[string]any{ + "success": false, + }, + }, + }) + }) + + err := client.CreateComment(context.Background(), "issue-123", "notes") + if err == nil { + t.Fatal("expected error, got nil") + } + if want := "linear_comment_failed: commentCreate returned success=false"; err.Error() != want { + t.Errorf("expected %q, got %q", want, err.Error()) + } +} + +func TestCreateComment_HTTPError(t *testing.T) { + client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "internal server error", http.StatusInternalServerError) + }) + + err := client.CreateComment(context.Background(), "issue-123", "notes") + if err == nil { + t.Fatal("expected error, got nil") + } +}