diff --git a/internal/config/generator.go b/internal/config/generator.go index 6688097f..78a52be3 100644 --- a/internal/config/generator.go +++ b/internal/config/generator.go @@ -1,7 +1,9 @@ package config import ( + "errors" "fmt" + "net/http" "net/url" "os" "path/filepath" @@ -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 { @@ -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 { @@ -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) diff --git a/pkg/jira/project_issuetypes.go b/pkg/jira/project_issuetypes.go new file mode 100644 index 00000000..37942ea8 --- /dev/null +++ b/pkg/jira/project_issuetypes.go @@ -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 +} diff --git a/pkg/jira/project_issuetypes_test.go b/pkg/jira/project_issuetypes_test.go new file mode 100644 index 00000000..7382899a --- /dev/null +++ b/pkg/jira/project_issuetypes_test.go @@ -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) +}