diff --git a/.custom-gcl.yml b/.custom-gcl.yml
index c116a82b1df..b90e06218b4 100644
--- a/.custom-gcl.yml
+++ b/.custom-gcl.yml
@@ -1,8 +1,8 @@
version: v2.2.2
plugins:
- - module: "github.com/google/go-github/v76/tools/fmtpercentv"
+ - module: "github.com/google/go-github/v77/tools/fmtpercentv"
path: ./tools/fmtpercentv
- - module: "github.com/google/go-github/v76/tools/jsonfieldname"
+ - module: "github.com/google/go-github/v77/tools/jsonfieldname"
path: ./tools/jsonfieldname
- - module: "github.com/google/go-github/v76/tools/sliceofpointers"
+ - module: "github.com/google/go-github/v77/tools/sliceofpointers"
path: ./tools/sliceofpointers
diff --git a/.golangci.yml b/.golangci.yml
index e64f5c11961..9c10ce24e42 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -143,11 +143,11 @@ linters:
fmtpercentv:
type: module
description: Reports usage of %d or %s in format strings.
- original-url: github.com/google/go-github/v76/tools/fmtpercentv
+ original-url: github.com/google/go-github/v77/tools/fmtpercentv
jsonfieldname:
type: module
description: Reports mismatches between Go field and JSON tag names.
- original-url: github.com/google/go-github/v76/tools/jsonfieldname
+ original-url: github.com/google/go-github/v77/tools/jsonfieldname
settings:
allowed-exceptions:
- ActionsCacheUsageList.RepoCacheUsage # TODO: RepoCacheUsages ?
@@ -215,7 +215,7 @@ linters:
sliceofpointers:
type: module
description: Reports usage of []*string and slices of structs without pointers.
- original-url: github.com/google/go-github/v76/tools/sliceofpointers
+ original-url: github.com/google/go-github/v77/tools/sliceofpointers
exclusions:
rules:
- linters:
diff --git a/README.md b/README.md
index 301580d5283..6ec47e8b6a9 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# go-github #
[](https://github.com/google/go-github/releases)
-[](https://pkg.go.dev/github.com/google/go-github/v76/github)
+[](https://pkg.go.dev/github.com/google/go-github/v77/github)
[](https://github.com/google/go-github/actions/workflows/tests.yml)
[](https://codecov.io/gh/google/go-github)
[](https://groups.google.com/group/go-github)
@@ -30,7 +30,7 @@ If you're interested in using the [GraphQL API v4][], the recommended library is
go-github is compatible with modern Go releases in module mode, with Go installed:
```bash
-go get github.com/google/go-github/v76
+go get github.com/google/go-github/v77
```
will resolve and add the package to the current development module, along with its dependencies.
@@ -38,7 +38,7 @@ will resolve and add the package to the current development module, along with i
Alternatively the same can be achieved if you use import in a package:
```go
-import "github.com/google/go-github/v76/github"
+import "github.com/google/go-github/v77/github"
```
and run `go get` without parameters.
@@ -46,13 +46,13 @@ and run `go get` without parameters.
Finally, to use the top-of-trunk version of this repo, use the following command:
```bash
-go get github.com/google/go-github/v76@master
+go get github.com/google/go-github/v77@master
```
## Usage ##
```go
-import "github.com/google/go-github/v76/github" // with go modules enabled (GO111MODULE=on or outside GOPATH)
+import "github.com/google/go-github/v77/github" // with go modules enabled (GO111MODULE=on or outside GOPATH)
import "github.com/google/go-github/github" // with go modules disabled
```
@@ -102,7 +102,7 @@ include the specified OAuth token. Therefore, authenticated clients should
almost never be shared between different users.
For API methods that require HTTP Basic Authentication, use the
-[`BasicAuthTransport`](https://pkg.go.dev/github.com/google/go-github/v76/github#BasicAuthTransport).
+[`BasicAuthTransport`](https://pkg.go.dev/github.com/google/go-github/v77/github#BasicAuthTransport).
#### As a GitHub App ####
@@ -125,7 +125,7 @@ import (
"net/http"
"github.com/bradleyfalzon/ghinstallation/v2"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
func main() {
@@ -159,7 +159,7 @@ import (
"os"
"strconv"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
"github.com/jferrl/go-githubauth"
"golang.org/x/oauth2"
)
@@ -400,7 +400,7 @@ For complete usage of go-github, see the full [package docs][].
[GitHub API v3]: https://docs.github.com/en/rest
[personal access token]: https://github.com/blog/1509-personal-api-tokens
-[package docs]: https://pkg.go.dev/github.com/google/go-github/v76/github
+[package docs]: https://pkg.go.dev/github.com/google/go-github/v77/github
[GraphQL API v4]: https://developer.github.com/v4/
[shurcooL/githubv4]: https://github.com/shurcooL/githubv4
[GitHub webhook events]: https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads
@@ -474,7 +474,7 @@ Versions prior to 48.2.0 are not listed.
| go-github Version | GitHub v3 API Version |
| ----------------- | --------------------- |
-| 76.0.0 | 2022-11-28 |
+| 77.0.0 | 2022-11-28 |
| ... | 2022-11-28 |
| 48.2.0 | 2022-11-28 |
diff --git a/example/actionpermissions/main.go b/example/actionpermissions/main.go
index a2479139a9c..e9743d97605 100644
--- a/example/actionpermissions/main.go
+++ b/example/actionpermissions/main.go
@@ -14,7 +14,7 @@ import (
"log"
"os"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
var (
diff --git a/example/appengine/app.go b/example/appengine/app.go
index de43015e56c..20263aac59a 100644
--- a/example/appengine/app.go
+++ b/example/appengine/app.go
@@ -12,7 +12,7 @@ import (
"net/http"
"os"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
"google.golang.org/appengine"
"google.golang.org/appengine/log"
)
diff --git a/example/basicauth/main.go b/example/basicauth/main.go
index 81a3c525e87..4a30d37537b 100644
--- a/example/basicauth/main.go
+++ b/example/basicauth/main.go
@@ -22,7 +22,7 @@ import (
"os"
"strings"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
"golang.org/x/term"
)
diff --git a/example/codespaces/newreposecretwithxcrypto/main.go b/example/codespaces/newreposecretwithxcrypto/main.go
index d9e685a5158..094b346b32f 100644
--- a/example/codespaces/newreposecretwithxcrypto/main.go
+++ b/example/codespaces/newreposecretwithxcrypto/main.go
@@ -37,7 +37,7 @@ import (
"log"
"os"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
"golang.org/x/crypto/nacl/box"
)
diff --git a/example/codespaces/newusersecretwithxcrypto/main.go b/example/codespaces/newusersecretwithxcrypto/main.go
index 7c75cc95878..389413a6538 100644
--- a/example/codespaces/newusersecretwithxcrypto/main.go
+++ b/example/codespaces/newusersecretwithxcrypto/main.go
@@ -38,7 +38,7 @@ import (
"log"
"os"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
"golang.org/x/crypto/nacl/box"
)
diff --git a/example/commitpr/main.go b/example/commitpr/main.go
index 164c91a9f91..cb6bf326b16 100644
--- a/example/commitpr/main.go
+++ b/example/commitpr/main.go
@@ -13,7 +13,7 @@
//
// Note, if you want to push a single file, you probably prefer to use the
// content API. An example is available here:
-// https://pkg.go.dev/github.com/google/go-github/v76/github#example-RepositoriesService-CreateFile
+// https://pkg.go.dev/github.com/google/go-github/v77/github#example-RepositoriesService-CreateFile
//
// Note, for this to work at least 1 commit is needed, so you if you use this
// after creating a repository you might want to make sure you set `AutoInit` to
@@ -33,7 +33,7 @@ import (
"time"
"github.com/ProtonMail/go-crypto/openpgp"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
var (
@@ -178,7 +178,7 @@ func pushCommit(ref *github.Reference, tree *github.Tree) (err error) {
return err
}
-// createPR creates a pull request. Based on: https://pkg.go.dev/github.com/google/go-github/v76/github#example-PullRequestsService-Create
+// createPR creates a pull request. Based on: https://pkg.go.dev/github.com/google/go-github/v77/github#example-PullRequestsService-Create
func createPR() (err error) {
if *prSubject == "" {
return errors.New("missing `-pr-title` flag; skipping PR creation")
diff --git a/example/go.mod b/example/go.mod
index 7b95e61c893..f289cd5e6ac 100644
--- a/example/go.mod
+++ b/example/go.mod
@@ -1,4 +1,4 @@
-module github.com/google/go-github/v76/example
+module github.com/google/go-github/v77/example
go 1.24.0
@@ -7,7 +7,7 @@ require (
github.com/bradleyfalzon/ghinstallation/v2 v2.17.0
github.com/gofri/go-github-pagination v1.0.1
github.com/gofri/go-github-ratelimit/v2 v2.0.2
- github.com/google/go-github/v76 v76.0.0
+ github.com/google/go-github/v77 v77.0.0
github.com/sigstore/sigstore-go v0.6.1
golang.org/x/crypto v0.43.0
golang.org/x/term v0.36.0
@@ -100,4 +100,4 @@ require (
)
// Use version at HEAD, not the latest published.
-replace github.com/google/go-github/v76 => ../
+replace github.com/google/go-github/v77 => ../
diff --git a/example/listenvironments/main.go b/example/listenvironments/main.go
index 102292d4ebf..457900a3aba 100644
--- a/example/listenvironments/main.go
+++ b/example/listenvironments/main.go
@@ -18,7 +18,7 @@ import (
"log"
"os"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
func main() {
diff --git a/example/migrations/main.go b/example/migrations/main.go
index 49c7e5660e3..46021e37c89 100644
--- a/example/migrations/main.go
+++ b/example/migrations/main.go
@@ -12,7 +12,7 @@ import (
"context"
"fmt"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
func fetchAllUserMigrations() ([]*github.UserMigration, error) {
diff --git a/example/newfilewithappauth/main.go b/example/newfilewithappauth/main.go
index 7972920d8e5..4db7245931f 100644
--- a/example/newfilewithappauth/main.go
+++ b/example/newfilewithappauth/main.go
@@ -16,7 +16,7 @@ import (
"time"
"github.com/bradleyfalzon/ghinstallation/v2"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
func main() {
diff --git a/example/newrepo/main.go b/example/newrepo/main.go
index 9e3658187c8..dd46512b74d 100644
--- a/example/newrepo/main.go
+++ b/example/newrepo/main.go
@@ -16,7 +16,7 @@ import (
"log"
"os"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
var (
diff --git a/example/newreposecretwithlibsodium/go.mod b/example/newreposecretwithlibsodium/go.mod
index ebc1bc2e779..5bd11527af2 100644
--- a/example/newreposecretwithlibsodium/go.mod
+++ b/example/newreposecretwithlibsodium/go.mod
@@ -4,10 +4,10 @@ go 1.24.0
require (
github.com/GoKillers/libsodium-go v0.0.0-20171022220152-dd733721c3cb
- github.com/google/go-github/v76 v76.0.0
+ github.com/google/go-github/v77 v77.0.0
)
require github.com/google/go-querystring v1.1.0 // indirect
// Use version at HEAD, not the latest published.
-replace github.com/google/go-github/v76 => ../..
+replace github.com/google/go-github/v77 => ../..
diff --git a/example/newreposecretwithlibsodium/main.go b/example/newreposecretwithlibsodium/main.go
index 8d65539df50..94b4e3288ca 100644
--- a/example/newreposecretwithlibsodium/main.go
+++ b/example/newreposecretwithlibsodium/main.go
@@ -36,7 +36,7 @@ import (
"os"
sodium "github.com/GoKillers/libsodium-go/cryptobox"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
var (
diff --git a/example/newreposecretwithxcrypto/main.go b/example/newreposecretwithxcrypto/main.go
index f5bca101af8..b802f7e381c 100644
--- a/example/newreposecretwithxcrypto/main.go
+++ b/example/newreposecretwithxcrypto/main.go
@@ -37,7 +37,7 @@ import (
"log"
"os"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
"golang.org/x/crypto/nacl/box"
)
diff --git a/example/ratelimit/main.go b/example/ratelimit/main.go
index dfcbb305a3e..7d82b6fedb9 100644
--- a/example/ratelimit/main.go
+++ b/example/ratelimit/main.go
@@ -17,7 +17,7 @@ import (
"github.com/gofri/go-github-ratelimit/v2/github_ratelimit"
"github.com/gofri/go-github-ratelimit/v2/github_ratelimit/github_primary_ratelimit"
"github.com/gofri/go-github-ratelimit/v2/github_ratelimit/github_secondary_ratelimit"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
func main() {
diff --git a/example/simple/main.go b/example/simple/main.go
index 2f651a4de40..386dcb3e76b 100644
--- a/example/simple/main.go
+++ b/example/simple/main.go
@@ -12,7 +12,7 @@ import (
"context"
"fmt"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
// Fetch all the public organizations' membership of a user.
diff --git a/example/tokenauth/main.go b/example/tokenauth/main.go
index 78654f234fb..405b7659589 100644
--- a/example/tokenauth/main.go
+++ b/example/tokenauth/main.go
@@ -15,7 +15,7 @@ import (
"log"
"os"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
"golang.org/x/term"
)
diff --git a/example/topics/main.go b/example/topics/main.go
index 434f1ad50d4..0752671dad4 100644
--- a/example/topics/main.go
+++ b/example/topics/main.go
@@ -12,7 +12,7 @@ import (
"context"
"fmt"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
// Fetch and lists all the public topics associated with the specified GitHub topic.
diff --git a/example/verifyartifact/main.go b/example/verifyartifact/main.go
index 789fa0fe0d6..9b3984b860d 100644
--- a/example/verifyartifact/main.go
+++ b/example/verifyartifact/main.go
@@ -18,7 +18,7 @@ import (
"log"
"os"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
"github.com/sigstore/sigstore-go/pkg/bundle"
"github.com/sigstore/sigstore-go/pkg/root"
"github.com/sigstore/sigstore-go/pkg/verify"
diff --git a/github/doc.go b/github/doc.go
index 65fd79bd28f..f5670793068 100644
--- a/github/doc.go
+++ b/github/doc.go
@@ -8,7 +8,7 @@ Package github provides a client for using the GitHub API.
Usage:
- import "github.com/google/go-github/v76/github" // with go modules enabled (GO111MODULE=on or outside GOPATH)
+ import "github.com/google/go-github/v77/github" // with go modules enabled (GO111MODULE=on or outside GOPATH)
import "github.com/google/go-github/github" // with go modules disabled
Construct a new GitHub client, then use the various services on the client to
diff --git a/github/examples_test.go b/github/examples_test.go
index 5fb81ebfd35..0954ebfc220 100644
--- a/github/examples_test.go
+++ b/github/examples_test.go
@@ -12,7 +12,7 @@ import (
"fmt"
"log"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
func ExampleMarkdownService_Render() {
diff --git a/github/github.go b/github/github.go
index fa10a446219..351b5797dd2 100644
--- a/github/github.go
+++ b/github/github.go
@@ -29,7 +29,7 @@ import (
)
const (
- Version = "v76.0.0"
+ Version = "v77.0.0"
defaultAPIVersion = "2022-11-28"
defaultBaseURL = "https://api.github.com/"
diff --git a/go.mod b/go.mod
index aa1b017793a..35e0f74554d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/google/go-github/v76
+module github.com/google/go-github/v77
go 1.24.0
diff --git a/test/fields/fields.go b/test/fields/fields.go
index 6be67473ff4..a0f080702a0 100644
--- a/test/fields/fields.go
+++ b/test/fields/fields.go
@@ -25,7 +25,7 @@ import (
"reflect"
"strings"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
var (
diff --git a/test/integration/activity_test.go b/test/integration/activity_test.go
index 5bcf2eb5b4f..e1abda1b1b0 100644
--- a/test/integration/activity_test.go
+++ b/test/integration/activity_test.go
@@ -10,7 +10,7 @@ package integration
import (
"testing"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
const (
diff --git a/test/integration/authorizations_test.go b/test/integration/authorizations_test.go
index e6a492497cb..ea96e2bb8e9 100644
--- a/test/integration/authorizations_test.go
+++ b/test/integration/authorizations_test.go
@@ -13,7 +13,7 @@ import (
"testing"
"time"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
const (
diff --git a/test/integration/github_test.go b/test/integration/github_test.go
index 09975511ca2..e29fe0a7246 100644
--- a/test/integration/github_test.go
+++ b/test/integration/github_test.go
@@ -15,7 +15,7 @@ import (
"sync"
"testing"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
// client is a github.Client with the default http.Client. It is authorized if auth is true.
diff --git a/test/integration/projects_test.go b/test/integration/projects_test.go
index 1357fcca816..a3a1ffd2d08 100644
--- a/test/integration/projects_test.go
+++ b/test/integration/projects_test.go
@@ -11,7 +11,7 @@ import (
"os"
"testing"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
// Integration tests for Projects V2 endpoints defined in github/projects.go.
diff --git a/test/integration/repos_test.go b/test/integration/repos_test.go
index 9ea4f099f6a..126a4ee73b1 100644
--- a/test/integration/repos_test.go
+++ b/test/integration/repos_test.go
@@ -13,7 +13,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
func TestRepositories_CRUD(t *testing.T) {
diff --git a/test/integration/users_test.go b/test/integration/users_test.go
index 3a8643ebd28..742e88a7d28 100644
--- a/test/integration/users_test.go
+++ b/test/integration/users_test.go
@@ -12,7 +12,7 @@ import (
"math/rand"
"testing"
- "github.com/google/go-github/v76/github"
+ "github.com/google/go-github/v77/github"
)
func TestUsers_Get(t *testing.T) {
diff --git a/tools/gen-release-notes/main.go b/tools/gen-release-notes/main.go
new file mode 100644
index 00000000000..f125fca1293
--- /dev/null
+++ b/tools/gen-release-notes/main.go
@@ -0,0 +1,260 @@
+// 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.
+
+// gen-release-notes first reads the web page https://github.com/google/go-github
+// to determine what the prior release was, (e.g. "v76.0.0")
+// then reads https://github.com/google/go-github/compare/${PRIOR_RELEASE}...master
+// to find out what changes were made since then.
+//
+// Finally, it writes the release notes to stdout, summarizing the
+// breaking and non-breaking changes since that release.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "regexp"
+ "strings"
+)
+
+const (
+ baseWebURL = "https://github.com/google/go-github"
+
+ // fragile, but works for now.
+ detailsDiv = `
`
+)
+
+var (
+ releaseRE = regexp.MustCompile(`
]+>([^<]+)\s*
]*>`)
+ linkPrimaryRE = regexp.MustCompile(`(?ms)(.*?]+>)`)
+ newlinesRE = regexp.MustCompile(`(?m)(\n+)`)
+ descriptionRE = regexp.MustCompile(`^\* (.*?\((#[^\)]+)\))`)
+)
+
+func main() {
+ log.SetFlags(0)
+ flag.Parse()
+
+ priorRelease := getPriorRelease()
+
+ newChanges := newChangesSinceRelease(priorRelease)
+
+ releaseNotes := genReleaseNotes(newChanges)
+ fmt.Printf("%v%v", releaseNotes, "\n")
+
+ log.Print("Done.")
+}
+
+func genReleaseNotes(text string) string {
+ // strip everything before first detailsDiv:
+ idx := strings.Index(text, detailsDiv)
+ if idx < 0 {
+ log.Fatal("Could not find detailsDiv")
+ }
+ text = text[idx:]
+
+ allLines := splitIntoPRs(text)
+ fullBreakingLines, fullNonBreakingLines := splitBreakingLines(allLines)
+ refBreakingLines, refNonBreakingLines := genRefLines(fullBreakingLines, fullNonBreakingLines)
+
+ return fmt.Sprintf(releaseNotesFmt,
+ strings.Join(fullBreakingLines, "\n"),
+ strings.Join(fullNonBreakingLines, "\n"),
+ strings.Join(refBreakingLines, "\n"),
+ strings.Join(refNonBreakingLines, "\n"))
+}
+
+func splitIntoPRs(text string) []string {
+ parts := strings.Split(text, detailsDiv)
+ if len(parts) < 2 {
+ log.Fatal("unable to find PRs")
+ }
+ prs := make([]string, 0, len(parts)-1)
+ for _, part := range parts {
+ if part == "" {
+ continue
+ }
+ newDiv := matchDivs(part)
+ for {
+ oldDiv := newDiv
+ newDiv = stripPRHTML(oldDiv)
+ if newDiv == oldDiv {
+ break
+ }
+ }
+ prs = append(prs, newDiv)
+ }
+ return prs
+}
+
+func stripPRHTML(text string) string {
+ _, innerText := getTagSequence(text)
+ if i := strings.Index(text, ""); i > 0 {
+ newText := text[:i] + strings.Join(innerText, "")
+ newText = strings.ReplaceAll(newText, "…", "")
+ newText = newlinesRE.ReplaceAllString(newText, "\n ")
+ return newText
+ }
+ return text
+}
+
+func getTagSequence(text string) (tagSeq, innerText []string) {
+ m := bracketsRE.FindAllStringIndex(text, -1)
+ var lastEnd int
+ for _, pair := range m {
+ start := pair[0]
+ end := pair[1] - 1
+ if lastEnd > 0 && start > lastEnd+1 {
+ rawText := text[lastEnd+1 : start]
+ s := strings.TrimSpace(rawText)
+ switch s {
+ case "", "…": // skip
+ default:
+ innerText = append(innerText, rawText)
+ }
+ }
+ lastEnd = end
+ s := text[start+1 : end]
+ if s == "code" {
+ innerText = append(innerText, " `")
+ continue
+ }
+ if s == "/code" {
+ innerText = append(innerText, "` ")
+ continue
+ }
+ if s[0] == '/' {
+ tagSeq = append(tagSeq, s)
+ continue
+ }
+ if i := strings.Index(s, " "); i > 0 {
+ tagSeq = append(tagSeq, s[0:i])
+ } else {
+ tagSeq = append(tagSeq, s)
+ }
+ }
+ return tagSeq, innerText
+}
+
+func matchDivs(text string) string {
+ chunks := strings.Split(text, ` `)
+ var divCount int
+ var lastChunk int
+ for i, chunk := range chunks {
+ chunks[i] = strings.TrimSpace(chunks[i])
+ divsInChunk := strings.Count(chunk, ` 0 {
+ newText = newText[:m2[0]]
+ }
+ return "* " + newText
+}
+
+func splitBreakingLines(allLines []string) (breaking, nonBreaking []string) {
+ for _, pr := range allLines {
+ if strings.Contains(pr, "!: ") {
+ breaking = append(breaking, pr)
+ } else {
+ nonBreaking = append(nonBreaking, pr)
+ }
+ }
+ return breaking, nonBreaking
+}
+
+func genRefLines(breaking, nonBreaking []string) (ref, refNon []string) {
+ for _, pr := range breaking {
+ m := descriptionRE.FindStringSubmatch(pr)
+ if len(m) == 3 {
+ ref = append(ref, strings.Replace(pr, m[1], m[2], 1))
+ }
+ }
+ for _, pr := range nonBreaking {
+ m := descriptionRE.FindStringSubmatch(pr)
+ if len(m) == 3 {
+ refNon = append(refNon, strings.Replace(pr, m[1], m[2], 1))
+ }
+ }
+ return ref, refNon
+}
+
+func newChangesSinceRelease(priorRelease string) string {
+ url := fmt.Sprintf("%v/compare/%v...master", baseWebURL, priorRelease)
+ resp, err := http.Get(url) //nolint: gosec
+ must(err)
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ must(err)
+
+ return string(body)
+}
+
+func getPriorRelease() string {
+ resp, err := http.Get(baseWebURL)
+ must(err)
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ must(err)
+
+ matches := releaseRE.FindStringSubmatch(string(body))
+ if len(matches) != 2 {
+ log.Fatal("could not find release info")
+ }
+
+ priorRelease := strings.TrimSpace(matches[1])
+ if priorRelease == "" {
+ log.Fatal("found empty prior release version")
+ }
+
+ return priorRelease
+}
+
+func must(err error) {
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+const releaseNotesFmt = `This release contains the following breaking API changes:
+
+%v
+
+...and the following additional changes:
+
+%v
+
+&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
+
+This release contains the following breaking API changes:
+
+%v
+
+...and the following additional changes:
+
+%v
+`
diff --git a/tools/gen-release-notes/main_test.go b/tools/gen-release-notes/main_test.go
new file mode 100644
index 00000000000..c81c589dbd7
--- /dev/null
+++ b/tools/gen-release-notes/main_test.go
@@ -0,0 +1,240 @@
+// 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 main
+
+import (
+ _ "embed"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+//go:embed testdata/compare-v76.html
+var compareV76HTML string
+
+//go:embed testdata/release-notes-v77.txt
+var releaseNotes string
+
+func TestGenReleaseNotes(t *testing.T) {
+ t.Parallel()
+ text := strings.ReplaceAll(compareV76HTML, "\r\n", "\n")
+ got := genReleaseNotes(text)
+ got = strings.ReplaceAll(got, "\r\n", "\n")
+ want := strings.ReplaceAll(releaseNotes, "\r\n", "\n")
+
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Log(got)
+ t.Errorf("genReleaseNotes mismatch (-want +got):\n%v", diff)
+ }
+}
+
+func TestSplitIntoPRs(t *testing.T) {
+ t.Parallel()
+
+ text := strings.ReplaceAll(compareV76HTML, "\r\n", "\n")
+ text = text[191600:]
+
+ got := splitIntoPRs(text)
+ want := []string{
+ `* Bump go-github from v75 to v76 in /scrape (#3783)`,
+ `* Add custom jsonfieldname linter to ensure Go field name matches JSON tag name (#3757)`,
+ `* chore: Fix typo in comment (#3786)`,
+ `* feat: Add support for private registries endpoints (#3785)`,
+ "* Only set `Authorization` when `token` is available (#3789)",
+ `* test: Ensure Authorization is not set with empty token (#3790)`,
+ `* Fix spelling issues (#3792)`,
+ "* refactor!: Remove pointer from required field of CreateStatus API (#3794)\n BREAKING CHANGE: `RepositoriesService.CreateStatus` now takes value for `status`, not pointer.",
+ `* Add test cases for JSON resource marshaling - SCIM (#3798)`,
+ `* fix: Org/Enterprise UpdateRepositoryRulesetClearBypassActor sends empty array (#3796)`,
+ `* feat!: Add support for project items CRUD and project fields read operations (#3793)`,
+ }
+
+ if len(got) != len(want) {
+ t.Log(strings.Join(got, "\n"))
+ t.Fatalf("splitIntoPRs = %v lines, want %v", len(got), len(want))
+ }
+ for i := range got {
+ if got[i] != want[i] {
+ t.Errorf("splitIntoPRs[%v] =\n%v\n, want \n%v", i, got[i], want[i])
+ }
+ }
+}
+
+func TestMatchDivs(t *testing.T) {
+ t.Parallel()
+
+ text := `
+
+