Skip to content
Open
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
37 changes: 37 additions & 0 deletions internal/config/generator.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package config

import (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
Expand Down Expand Up @@ -639,6 +641,17 @@ func (c *JiraCLIConfigGenerator) configureIssueTypes() error {
Expand: "projects.issuetypes.fields",
})
if err != nil {
if code, ok := isCreateMetaFallbackEligible(err); ok {
cmdutil.Warn("Call to Metadata API endpoint is fialing; thus, using alternative API endpoint: project issue types (expand=issueTypes).")

its, ferr := c.jiraClient.ProjectIssueTypes(c.value.project.Key)
if ferr != nil {
return fmt.Errorf("failed to fetch issue types via fallback endpoint after createmeta returned %d: %w", code, ferr)
}

c.value.issueTypes = its
return nil
}
return err
}
if len(meta.Projects) == 0 || len(meta.Projects[0].IssueTypes) == 0 {
Expand Down Expand Up @@ -671,6 +684,17 @@ func (c *JiraCLIConfigGenerator) configureIssueTypesForJiraServerV9() error {
Expand: "projects.issuetypes.fields",
})
if err != nil {
if code, ok := isCreateMetaFallbackEligible(err); ok {
cmdutil.Warn("Call to Metadata API endpoint is fialing; thus, using alternative API endpoint: project issue types (expand=issueTypes).")

its, ferr := c.jiraClient.ProjectIssueTypes(c.value.project.Key)
if ferr != nil {
return fmt.Errorf("failed to fetch issue types via fallback endpoint after createmeta returned %d: %w", code, ferr)
}

c.value.issueTypes = its
return nil
}
return err
}
if len(meta.Values) == 0 {
Expand All @@ -693,6 +717,19 @@ func (c *JiraCLIConfigGenerator) configureIssueTypesForJiraServerV9() error {
return nil
}

func isCreateMetaFallbackEligible(err error) (int, bool) {
var uerr *jira.ErrUnexpectedResponse
if !errors.As(err, &uerr) {
return 0, false
}
switch uerr.StatusCode {
case http.StatusNotFound, http.StatusForbidden, http.StatusUnauthorized:
return uerr.StatusCode, true
default:
return uerr.StatusCode, false
}
}

func (c *JiraCLIConfigGenerator) configureFields() error {
customFields := make([]*issueTypeFieldConf, 0)

Expand Down
42 changes: 42 additions & 0 deletions pkg/jira/project_issuetypes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package jira

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
)

type projectIssueTypesResponse struct {
IssueTypes []*IssueType `json:"issueTypes"`
}

// ProjectIssueTypes fetches issue types for a given project using
// GET /rest/api/2/project/{projectKey}?expand=issueTypes.
//
// This endpoint does not provide create-screen field requirements (unlike createmeta),
// but it can be used as a fallback when createmeta is not available.
func (c *Client) ProjectIssueTypes(projectKey string) ([]*IssueType, error) {
path := fmt.Sprintf("/project/%s?expand=%s", url.PathEscape(projectKey), url.QueryEscape("issueTypes"))

res, err := c.GetV2(context.Background(), path, nil)
if err != nil {
return nil, err
}
if res == nil {
return nil, ErrEmptyResponse
}
defer func() { _ = res.Body.Close() }()

if res.StatusCode != http.StatusOK {
return nil, formatUnexpectedResponse(res)
}

var out projectIssueTypesResponse
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
return nil, err
}

return out.IssueTypes, nil
}
51 changes: 51 additions & 0 deletions pkg/jira/project_issuetypes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package jira

import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestProjectIssueTypes(t *testing.T) {
var unexpectedStatusCode bool

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/rest/api/2/project/TEST", r.URL.Path)

qs := r.URL.Query()
if unexpectedStatusCode {
w.WriteHeader(401)
return
}

assert.Equal(t, url.Values{
"expand": []string{"issueTypes"},
}, qs)

w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"issueTypes": [
{"id": "1", "name": "Task", "subtask": false},
{"id": "2", "name": "Sub-task", "subtask": true}
]
}`))
}))
defer server.Close()

client := NewClient(Config{Server: server.URL}, WithTimeout(3*time.Second))

actual, err := client.ProjectIssueTypes("TEST")
assert.NoError(t, err)
assert.Equal(t, []*IssueType{
{ID: "1", Name: "Task", Subtask: false},
{ID: "2", Name: "Sub-task", Subtask: true},
}, actual)

unexpectedStatusCode = true
_, err = client.ProjectIssueTypes("TEST")
assert.Error(t, &ErrUnexpectedResponse{}, err)
}
Loading