Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CUP-2] wip2 #182

Closed
wants to merge 2 commits into from
Closed
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
6 changes: 3 additions & 3 deletions cmd/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ func (c *create) Create(ctx context.Context, args *args) (*core.LocalPr, error)

// The first commit is the child-most one.
lastCommit := args.Commits[0].Hash
title, body, err := c.GetBodyAndTitle(args.Commits)
title, body, err := c.GetBodyAndTitle(ctx, args.Commits)
if err != nil {
return nil, fmt.Errorf("could not get the pull request body and title: %w", err)
}
Expand All @@ -340,13 +340,13 @@ func (c *create) Create(ctx context.Context, args *args) (*core.LocalPr, error)
return localPr, err
}

func (c *create) GetBodyAndTitle(commits []*object.Commit) (string, string, error) {
func (c *create) GetBodyAndTitle(ctx context.Context, commits []*object.Commit) (string, string, error) {
rawTitle, rawBody := c.getRawBodyAndTitle(commits)
commitMessages := make([]string, len(commits))
for i, c := range commits {
commitMessages[i] = c.Message
}
title, body, err := c.StoryService.EnrichBodyAndTitle(commitMessages, rawTitle, rawBody)
title, body, err := c.StoryService.EnrichBodyAndTitle(ctx, commitMessages, rawTitle, rawBody)
if err != nil {
return "", "", fmt.Errorf("could not enrich the PR with the Story: %w", err)
}
Expand Down
9 changes: 9 additions & 0 deletions core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ func init() {
viper.SetDefault("github.merge.method", "rebase")
viper.SetDefault("github.timeout", 30*time.Second)
viper.SetDefault("story.enrichbody", false)
viper.SetDefault("story.fetch", false)
}

func GetGithubToken() string {
Expand Down Expand Up @@ -59,10 +60,18 @@ func BodyEnrichmentEnabled() bool {
return viper.GetBool("story.enrichbody")
}

func FetchStoriesEnabled() bool {
return viper.GetBool("story.fetch")
}

func GetStoryTool() string {
return viper.GetString("story.tool.name")
}

func GetStoryToolBaseUrl() string {
return viper.GetString("story.tool.baseurl")
}

func GetStoryToolToken() string {
return viper.GetString("story.tool.token")
}
84 changes: 84 additions & 0 deletions core/story/linear.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package story

import (
"context"
"fmt"
"net/http"

"github.com/cupcicm/opp/core"
"github.com/machinebox/graphql"
)

const linearGraphqlEndpoint = "https://api.linear.app/graphql"

type linearStoryFetcher struct {
client *graphql.Client
}

type AuthHeader struct{ Token string }

func (h AuthHeader) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("Authorization", h.Token)
return http.DefaultTransport.RoundTrip(req)
}

func newClient() *graphql.Client {
opt := graphql.WithHTTPClient(&http.Client{Transport: AuthHeader{Token: core.GetStoryToolToken()}})

return graphql.NewClient(linearGraphqlEndpoint, opt)
}

func NewLinearStoryFetcher() StoryFetcher {
return &linearStoryFetcher{
client: newClient(),
}
}

func (l *linearStoryFetcher) FetchInProgressStories(ctx context.Context) (stories []Story, err error) {
var isMe bool = true
var state string = "started"
var order string = "Descending"

filter := &IssueFilter{
Assignee: &UserFilter{
IsMe: &BooleanComparator{
Eq: &isMe,
},
},
State: &WorkflowStateFilter{
Type: &StringComparator{
Eq: &state,
},
},
}

sort := []SortOption{
{
CreatedAt: &OrderComparator{
Order: &order,
},
},
}

// make a request
req := graphql.NewRequest(linearQuery)

// set any variables
req.Var("filter", filter)
req.Var("sort", sort)

// run it and capture the response
var respData ResponseData
if err := l.client.Run(ctx, req, &respData); err != nil {
// TODO(ClairePhi): add retry with avast retry-go
return nil, fmt.Errorf("failed to fetch data from the linear graphql API: %w", err)
}
for _, issue := range respData.Issues.Nodes {
stories = append(stories, Story{
title: issue.Title,
identifier: issue.Identifier,
})
}

return stories, nil
}
130 changes: 130 additions & 0 deletions core/story/linear_graphql_query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package story

// query

const linearQuery = `
query issues ($filter: IssueFilter, $sort: [IssueSortInput!]) {
issues(filter: $filter, sort: $sort) {
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
nodes {
title
identifier
}
}
}
`

// variables
// "filter": {
// "state": {
// "type": {
// "eq": "started"
// }
// },
// "assignee": {
// "isMe": {
// "eq": true
// }
// }
// },
// "sort": [
// {
// "createdAt": {
// "order": "Descending"
// }
// }
// ],
// }

// Issue filtering options.
type IssueFilter struct {
// Filters that the issues assignee must satisfy.
Assignee *UserFilter `json:"assignee,omitempty"`
// Filters that the issues state must satisfy.
State *WorkflowStateFilter `json:"state,omitempty"`
}

type UserFilter struct {
// Filter based on the currently authenticated user. Set to true to filter for the authenticated user, false for any other user.
IsMe *BooleanComparator `json:"isMe,omitempty"`
}

type BooleanComparator struct {
// Equals constraint.
Eq *bool `json:"eq,omitempty"`
}

type WorkflowStateFilter struct {
// Comparator for the workflow state type.
Type *StringComparator `json:"type,omitempty"`
}

type StringComparator struct {
// Equals constraint.
Eq *string `json:"eq,omitempty"`
}

type IssueSortInput struct {
SortOptions []SortOption
}

type SortOption struct {
CreatedAt *OrderComparator `json:"createdAt,omitempty"`
}

type OrderComparator struct {
Order *string `json:"order,omitempty"`
}

//response
// {
// "data": {
// "issues": {
// "pageInfo": {
// "endCursor": "eyJrZXkiOiIwYWVkOWIzYS0xOWU4LTQ3MWYtYjg4Ny1mYmE2NWQ2YTFmZDAiLCJncm91cCI6IiJ9",
// "hasNextPage": false,
// "hasPreviousPage": false,
// "startCursor": "eyJrZXkiOiIwYWVkOWIzYS0xOWU4LTQ3MWYtYjg4Ny1mYmE2NWQ2YTFmZDAiLCJncm91cCI6IiJ9"
// },
// "nodes": [
// {
// "title": "Welcome to Linear 👋",
// "identifier": "CUP-1"
// }
// ]
// }
// }
// }

type ResponseData struct {
Issues Issues `json:"issues"`
}

type Issues struct {
Nodes []Issue `json:"nodes"`
PageInfo *PageInfo `json:"pageInfo"`
}

type PageInfo struct {
// Indicates if there are more results when paginating backward.
HasPreviousPage bool `json:"hasPreviousPage"`
// Indicates if there are more results when paginating forward.
HasNextPage bool `json:"hasNextPage"`
// Cursor representing the first result in the paginated results.
StartCursor *string `json:"startCursor,omitempty"`
// Cursor representing the last result in the paginated results.
EndCursor *string `json:"endCursor,omitempty"`
}

// An issue.
type Issue struct {
// The issue's title.
Title string `json:"title"`
// Issue's human readable identifier (e.g. ENG-123).
Identifier string `json:"identifier"`
}
43 changes: 43 additions & 0 deletions core/story/story_fetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package story

import (
"context"
"fmt"

"github.com/cupcicm/opp/core"
)

type Story struct {
title string
identifier string
}

type StoryFetcher interface {
FetchInProgressStories(context.Context) ([]Story, error)
}

type StoryFetcherNoop struct{}

func (s *StoryFetcherNoop) FetchInProgressStories(_ context.Context) ([]Story, error) {
return []Story{}, nil
}

func getStoryFetcher() StoryFetcher {
if !core.FetchStoriesEnabled() {
return &StoryFetcherNoop{}
}

storyTool, err := getStoryTool()
if err != nil {
fmt.Printf("Story tool not configured correctly, will not attempt to fetch stories: %s\n", err.Error())
return &StoryFetcherNoop{}
}

switch t := storyTool.tool; t {
case "linear":
return NewLinearStoryFetcher()
default:
fmt.Printf("Story tool not supported, will not attempt to fetch stories: %s\n", t)
return &StoryFetcherNoop{}
}
}
Loading
Loading