Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions WORKFLOW.claude.md.example
Original file line number Diff line number Diff line change
Expand Up @@ -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":"<your notes here>"}}'
```
6. Create a pull request with a clear description
7. Move the issue to "Human Review" when complete
11 changes: 9 additions & 2 deletions WORKFLOW.md.example
Original file line number Diff line number Diff line change
Expand Up @@ -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":"<your notes here>"}}'
```
6. Create a pull request with a clear description
7. Move the issue to "Human Review" when complete
37 changes: 37 additions & 0 deletions internal/linear/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
104 changes: 104 additions & 0 deletions internal/linear/client_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
Loading