Repository: google/go-github
Pull request #3525 (2025-03-20 15:05:46)

feat: Add support for Issue Types API
This pull request introduces support for the Issue Types API to the `go-github` library, enabling users to manage issue types within their organizations. The following functions have been added:

- **ListIssueTypes**: Retrieves all issue types for a specified organization.
- **CreateIssueType**: Allows the creation of a new issue type within an organization.
- **UpdateIssueType**: Enables updating the attributes of an existing issue type.
- **DeleteIssueType**: Facilitates the deletion of an issue type from an organization.

Each function is accompanied by comprehensive tests to ensure functionality and reliability.

API References:  https://docs.github.com/en/rest/orgs/issue-types

Fixes: https://github.com/google/go-github/issues/3523

Please let me know if any additional changes or improvements are needed!



codecov[bot]: ## [Codecov](https://app.codecov.io/gh/google/go-github/pull/3525?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 91.30%. Comparing base [(`bcfcb89`)](https://app.codecov.io/gh/google/go-github/commit/bcfcb89f92d5d9aa5153f250850c898433f208cd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) to head [(`f605f76`)](https://app.codecov.io/gh/google/go-github/commit/f605f761922b018c07e9571513d9ff6630dac11e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #3525      +/-   ##
==========================================
+ Coverage   91.27%   91.30%   +0.02%     
==========================================
  Files         183      184       +1     
  Lines       16099    16143      +44     
==========================================
+ Hits        14695    14739      +44     
  Misses       1230     1230              
  Partials      174      174              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/google/go-github/pull/3525?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
gmlewis: It looks like the openapi stuff is a problem afterall.
I'm thinking I should make a separate PR to update openapi, then you can merge it from master and your PR can then be focused on just your changes.  I'll do that today.
atilsensalduz: Tests are passed in my local. @gmlewis could you please rerun the test?   
gmlewis: > wdyt to add small delay in here?

What I would really like to understand is _WHY_ we have any race conditions at all.
There should be a way to close down the server reliably without any use of `sleep`, and I would love to get to the bottom of it. Can you please file an issue, @atilsensalduz, and/or investigate if you feel like it?

Patch:
diff --git a/github/github-accessors.go b/github/github-accessors.go
index ec029df2..689d7863 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -5902,6 +5902,30 @@ func (c *CreateOrUpdateCustomRepoRoleOptions) GetName() string {
 	return *c.Name
 }
 
+// GetColor returns the Color field if it's non-nil, zero value otherwise.
+func (c *CreateOrUpdateIssueTypesOptions) GetColor() string {
+	if c == nil || c.Color == nil {
+		return ""
+	}
+	return *c.Color
+}
+
+// GetDescription returns the Description field if it's non-nil, zero value otherwise.
+func (c *CreateOrUpdateIssueTypesOptions) GetDescription() string {
+	if c == nil || c.Description == nil {
+		return ""
+	}
+	return *c.Description
+}
+
+// GetIsPrivate returns the IsPrivate field if it's non-nil, zero value otherwise.
+func (c *CreateOrUpdateIssueTypesOptions) GetIsPrivate() bool {
+	if c == nil || c.IsPrivate == nil {
+		return false
+	}
+	return *c.IsPrivate
+}
+
 // GetBaseRole returns the BaseRole field if it's non-nil, zero value otherwise.
 func (c *CreateOrUpdateOrgRoleOptions) GetBaseRole() string {
 	if c == nil || c.BaseRole == nil {
diff --git a/github/orgs_issue_types.go b/github/orgs_issue_types.go
new file mode 100644
index 00000000..73e6f8d6
--- /dev/null
+++ b/github/orgs_issue_types.go
@@ -0,0 +1,99 @@
+// Copyright 2025 The go-github AUTHORS. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+	"context"
+	"fmt"
+)
+
+// CreateOrUpdateIssueTypesOptions represents the parameters for creating or updating an issue type.
+type CreateOrUpdateIssueTypesOptions struct {
+	Name        string  `json:"name"`                  // Name of the issue type. (Required.)
+	IsEnabled   bool    `json:"is_enabled"`            // Whether or not the issue type is enabled at the organization level. (Required.)
+	IsPrivate   *bool   `json:"is_private,omitempty"`  // Whether or not the issue type is restricted to issues in private repositories. (Optional.)
+	Description *string `json:"description,omitempty"` // Description of the issue type. (Optional.)
+	Color       *string `json:"color,omitempty"`       // Color for the issue type. Can be one of "gray", "blue", green "orange", "red", "pink", "purple", "null". (Optional.)
+}
+
+// ListIssueTypes lists all issue types for an organization.
+//
+// GitHub API docs: https://docs.github.com/rest/orgs/issue-types#list-issue-types-for-an-organization
+//
+//meta:operation GET /orgs/{org}/issue-types
+func (s *OrganizationsService) ListIssueTypes(ctx context.Context, org string) ([]*IssueType, *Response, error) {
+	u := fmt.Sprintf("orgs/%v/issue-types", org)
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var issueTypes []*IssueType
+	resp, err := s.client.Do(ctx, req, &issueTypes)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return issueTypes, resp, nil
+}
+
+// CreateIssueType creates a new issue type for an organization.
+//
+// GitHub API docs: https://docs.github.com/rest/orgs/issue-types#create-issue-type-for-an-organization
+//
+//meta:operation POST /orgs/{org}/issue-types
+func (s *OrganizationsService) CreateIssueType(ctx context.Context, org string, opt *CreateOrUpdateIssueTypesOptions) (*IssueType, *Response, error) {
+	u := fmt.Sprintf("orgs/%v/issue-types", org)
+	req, err := s.client.NewRequest("POST", u, opt)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	issueType := new(IssueType)
+	resp, err := s.client.Do(ctx, req, issueType)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return issueType, resp, nil
+}
+
+// UpdateIssueType updates GitHub Pages for the named repo.
+//
+// GitHub API docs: https://docs.github.com/rest/orgs/issue-types#update-issue-type-for-an-organization
+//
+//meta:operation PUT /orgs/{org}/issue-types/{issue_type_id}
+func (s *OrganizationsService) UpdateIssueType(ctx context.Context, org string, issueTypeID int64, opt *CreateOrUpdateIssueTypesOptions) (*IssueType, *Response, error) {
+	u := fmt.Sprintf("orgs/%v/issue-types/%v", org, issueTypeID)
+	req, err := s.client.NewRequest("PUT", u, opt)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	issueType := new(IssueType)
+	resp, err := s.client.Do(ctx, req, issueType)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return issueType, resp, nil
+}
+
+// DeleteIssueType deletes an issue type for an organization.
+//
+// GitHub API docs: https://docs.github.com/rest/orgs/issue-types#delete-issue-type-for-an-organization
+//
+//meta:operation DELETE /orgs/{org}/issue-types/{issue_type_id}
+func (s *OrganizationsService) DeleteIssueType(ctx context.Context, org string, issueTypeID int64) (*Response, error) {
+	u := fmt.Sprintf("orgs/%v/issue-types/%d", org, issueTypeID)
+	req, err := s.client.NewRequest("DELETE", u, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(ctx, req, nil)
+}


Test patch:
diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go
index b2e42329..85647c6d 100644
--- a/github/github-accessors_test.go
+++ b/github/github-accessors_test.go
@@ -7688,6 +7688,39 @@ func TestCreateOrUpdateCustomRepoRoleOptions_GetName(tt *testing.T) {
 	c.GetName()
 }
 
+func TestCreateOrUpdateIssueTypesOptions_GetColor(tt *testing.T) {
+	tt.Parallel()
+	var zeroValue string
+	c := &CreateOrUpdateIssueTypesOptions{Color: &zeroValue}
+	c.GetColor()
+	c = &CreateOrUpdateIssueTypesOptions{}
+	c.GetColor()
+	c = nil
+	c.GetColor()
+}
+
+func TestCreateOrUpdateIssueTypesOptions_GetDescription(tt *testing.T) {
+	tt.Parallel()
+	var zeroValue string
+	c := &CreateOrUpdateIssueTypesOptions{Description: &zeroValue}
+	c.GetDescription()
+	c = &CreateOrUpdateIssueTypesOptions{}
+	c.GetDescription()
+	c = nil
+	c.GetDescription()
+}
+
+func TestCreateOrUpdateIssueTypesOptions_GetIsPrivate(tt *testing.T) {
+	tt.Parallel()
+	var zeroValue bool
+	c := &CreateOrUpdateIssueTypesOptions{IsPrivate: &zeroValue}
+	c.GetIsPrivate()
+	c = &CreateOrUpdateIssueTypesOptions{}
+	c.GetIsPrivate()
+	c = nil
+	c.GetIsPrivate()
+}
+
 func TestCreateOrUpdateOrgRoleOptions_GetBaseRole(tt *testing.T) {
 	tt.Parallel()
 	var zeroValue string
diff --git a/github/orgs_issue_types_test.go b/github/orgs_issue_types_test.go
new file mode 100644
index 00000000..7775b4f5
--- /dev/null
+++ b/github/orgs_issue_types_test.go
@@ -0,0 +1,238 @@
+// Copyright 2025 The go-github AUTHORS. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestOrganizationsService_ListIssueTypes(t *testing.T) {
+	t.Parallel()
+	client, mux, _ := setup(t)
+
+	mux.HandleFunc("/orgs/o/issue-types", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `[
+			{
+				"id": 410,
+				"node_id": "IT_kwDNAd3NAZo",
+				"name": "Task",
+				"description": "A specific piece of work",
+				"created_at": "2024-12-11T14:39:09Z",
+				"updated_at": "2024-12-11T14:39:09Z"
+			},
+			{
+				"id": 411,
+				"node_id": "IT_kwDNAd3NAZs",
+				"name": "Bug",
+				"description": "An unexpected problem or behavior",
+				"created_at": "2024-12-11T14:39:09Z",
+				"updated_at": "2024-12-11T14:39:09Z"
+			}
+		]`)
+	})
+
+	ctx := context.Background()
+	issueTypes, _, err := client.Organizations.ListIssueTypes(ctx, "o")
+	if err != nil {
+		t.Errorf("Organizations.ListIssueTypes returned error: %v", err)
+	}
+
+	want := []*IssueType{
+		{
+			ID:          Ptr(int64(410)),
+			NodeID:      Ptr("IT_kwDNAd3NAZo"),
+			Name:        Ptr("Task"),
+			Description: Ptr("A specific piece of work"),
+			CreatedAt:   Ptr(Timestamp{time.Date(2024, 12, 11, 14, 39, 9, 0, time.UTC)}),
+			UpdatedAt:   Ptr(Timestamp{time.Date(2024, 12, 11, 14, 39, 9, 0, time.UTC)}),
+		},
+		{
+			ID:          Ptr(int64(411)),
+			NodeID:      Ptr("IT_kwDNAd3NAZs"),
+			Name:        Ptr("Bug"),
+			Description: Ptr("An unexpected problem or behavior"),
+			CreatedAt:   Ptr(Timestamp{time.Date(2024, 12, 11, 14, 39, 9, 0, time.UTC)}),
+			UpdatedAt:   Ptr(Timestamp{time.Date(2024, 12, 11, 14, 39, 9, 0, time.UTC)})},
+	}
+	if !cmp.Equal(issueTypes, want) {
+		t.Errorf("Organizations.ListIssueTypes returned %+v, want %+v", issueTypes, want)
+	}
+
+	const methodName = "ListIssueTypes"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Organizations.ListIssueTypes(ctx, "\n")
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Organizations.ListIssueTypes(ctx, "o")
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestOrganizationsService_CreateIssueType(t *testing.T) {
+	t.Parallel()
+	client, mux, _ := setup(t)
+
+	input := &CreateOrUpdateIssueTypesOptions{
+		Name:        "Epic",
+		Description: Ptr("An issue type for a multi-week tracking of work"),
+		IsEnabled:   true,
+		Color:       Ptr("green"),
+		IsPrivate:   Ptr(true),
+	}
+
+	mux.HandleFunc("/orgs/o/issue-types", func(w http.ResponseWriter, r *http.Request) {
+		v := new(CreateOrUpdateIssueTypesOptions)
+		assertNilError(t, json.NewDecoder(r.Body).Decode(v))
+
+		testMethod(t, r, "POST")
+		if !cmp.Equal(v, input) {
+			t.Errorf("Request body = %+v, want %+v", v, input)
+		}
+
+		fmt.Fprint(w, `{
+				"id": 410,
+				"node_id": "IT_kwDNAd3NAZo",
+				"name": "Epic",
+				"description": "An issue type for a multi-week tracking of work",
+				"created_at": "2024-12-11T14:39:09Z",
+				"updated_at": "2024-12-11T14:39:09Z"
+		}`)
+	})
+
+	ctx := context.Background()
+	issueType, _, err := client.Organizations.CreateIssueType(ctx, "o", input)
+	if err != nil {
+		t.Errorf("Organizations.CreateIssueType returned error: %v", err)
+	}
+	want := &IssueType{
+		ID:          Ptr(int64(410)),
+		NodeID:      Ptr("IT_kwDNAd3NAZo"),
+		Name:        Ptr("Epic"),
+		Description: Ptr("An issue type for a multi-week tracking of work"),
+		CreatedAt:   Ptr(Timestamp{time.Date(2024, 12, 11, 14, 39, 9, 0, time.UTC)}),
+		UpdatedAt:   Ptr(Timestamp{time.Date(2024, 12, 11, 14, 39, 9, 0, time.UTC)}),
+	}
+
+	if !cmp.Equal(issueType, want) {
+		t.Errorf("Organizations.CreateIssueType returned %+v, want %+v", issueType, want)
+	}
+
+	const methodName = "CreateIssueType"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Organizations.CreateIssueType(ctx, "\n", input)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Organizations.CreateIssueType(ctx, "o", input)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestOrganizationsService_UpdateIssueType(t *testing.T) {
+	t.Parallel()
+	client, mux, _ := setup(t)
+
+	input := &CreateOrUpdateIssueTypesOptions{
+		Name:        "Epic",
+		Description: Ptr("An issue type for a multi-week tracking of work"),
+		IsEnabled:   true,
+		Color:       Ptr("green"),
+		IsPrivate:   Ptr(true),
+	}
+
+	mux.HandleFunc("/orgs/o/issue-types/410", func(w http.ResponseWriter, r *http.Request) {
+		v := new(CreateOrUpdateIssueTypesOptions)
+		assertNilError(t, json.NewDecoder(r.Body).Decode(v))
+
+		testMethod(t, r, "PUT")
+		if !cmp.Equal(v, input) {
+			t.Errorf("Request body = %+v, want %+v", v, input)
+		}
+
+		fmt.Fprint(w, `{
+				"id": 410,
+				"node_id": "IT_kwDNAd3NAZo",
+				"name": "Epic",
+				"description": "An issue type for a multi-week tracking of work",
+				"created_at": "2024-12-11T14:39:09Z",
+				"updated_at": "2024-12-11T14:39:09Z"
+		}`)
+	})
+
+	ctx := context.Background()
+	issueType, _, err := client.Organizations.UpdateIssueType(ctx, "o", 410, input)
+	if err != nil {
+		t.Errorf("Organizations.UpdateIssueType returned error: %v", err)
+	}
+	want := &IssueType{
+		ID:          Ptr(int64(410)),
+		NodeID:      Ptr("IT_kwDNAd3NAZo"),
+		Name:        Ptr("Epic"),
+		Description: Ptr("An issue type for a multi-week tracking of work"),
+		CreatedAt:   Ptr(Timestamp{time.Date(2024, 12, 11, 14, 39, 9, 0, time.UTC)}),
+		UpdatedAt:   Ptr(Timestamp{time.Date(2024, 12, 11, 14, 39, 9, 0, time.UTC)}),
+	}
+
+	if !cmp.Equal(issueType, want) {
+		t.Errorf("Organizations.UpdateIssueType returned %+v, want %+v", issueType, want)
+	}
+
+	const methodName = "UpdateIssueType"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Organizations.UpdateIssueType(ctx, "\n", -1, input)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Organizations.UpdateIssueType(ctx, "o", 410, input)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestOrganizationsService_DeleteIssueType(t *testing.T) {
+	t.Parallel()
+	client, mux, _ := setup(t)
+
+	mux.HandleFunc("/orgs/o/issue-types/410", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "DELETE")
+	})
+
+	ctx := context.Background()
+	_, err := client.Organizations.DeleteIssueType(ctx, "o", 410)
+	if err != nil {
+		t.Errorf("Organizations.DeleteIssueType returned error: %v", err)
+	}
+
+	const methodName = "DeleteIssueType"
+	testBadOptions(t, methodName, func() (err error) {
+		_, err = client.Organizations.DeleteIssueType(ctx, "\n", -1)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		return client.Organizations.DeleteIssueType(ctx, "o", 410)
+	})
+}